Implementing Access Control in a Node.js application

Most web applications rely on some sort of access control to keep users from accessing information not meant for them. If authentication is a lock on the main door of the hotel, then access control is the individual access card they give to each user for accessing their room.

In this post we will go beyond theory and take a more hands on approach by building RBAC module from the scratch so we can review our user’s privileges. Our aim, as usual, is to make the web a securer place for everyone.

We will begin with a short recap on access control theory followed by incremental how-to steps for building it.

So let’s get started.

Relearn Access Control

When asking developers to name different access control methods, the usual answer seems to be ACL and RBAC. If you answered this question the same way, then you are also among the misinformed. Let’s look at both of these in turn and then explain why.

ACL or Access Control List is an implementation of access control, usually represented as a table of privileges.

In this table we can see how each user is a row and has specific privileges assigned to them. Upon access control check, the user’s row and the column in question are cross-checked — this determines if this user has access or not.

RBAC or Role Based Access Control is an access control method where users are given roles and the roles determine what privileges they have. It is usually described as a tree or diagram, as roles can inherit accesses from their parent roles. So our previous ACL table could look something like this:

These are the common understandings of ACL and RBAC and they are both incorrect. And here’s why:

First of all, ACL is not an access control model, but an implementation type. It is often confused with IBAC (Identity Based Access Control) where each individual has their access rights determined separately — based on identity.

That sounds very much like the ACL we described earlier. However, ACL variations like ACLg can also be used to implement RBAC access model. We simply substitute the individual for a group. As a result we end up with:

This means ACLg (g stands for grouped) is equivalent of RBACm (m stands for minimal). You might be wondering where is the hierarchy in this model. Well there isn’t any. RBAC doesn’t have hierarchy written in the basic definition — it is an added extra in a model referred to HRBAC (Hierarchical Role Based Access Control).

So to recap: ACL is not an access control model, but an implementation type and RBAC does not have hierarchy by the baseline definition.

Know Your Access Control

Now that we have determined that we have some misconceptions about the most popular access control methods, let’s take a look at what types of access control there actually are. By the end of this section you should have an overview of the common access control methods and how they differ.

MAC/DAC (Mandatory/Discretionary Access Control) — although completely separate access control methods, I grouped them together as these two only differ in one important aspect. Both focus on the data object as the center of access rights. Discretionary access control method can most readily be seen in UNIX systems, where the owner of any given file has control over whom to give access. The access rights are in his/her discretion — hence the name. MAC also focuses on the data object as the basis of access rights, however the rights are not determined by the owner, but instead by the sensitivity of the data object. This method is most often seen in governmental or military systems due to the high costs of implementation.

Mandatory Access Control (MAC)

In short MAC and DAC both focus on the data object or file, whereas DAC allows me (the owner of the file) to determine who has access. In MAC however the access rights are determined by the administrator or general rule.

Discretionary Access Control (DAC)

IBAC (Identity Based Access Control) — this method focuses on the identity of the user as the basis of the privileges. Each individual is given specific access rights for every operation. The benefits are high granularity in assigning rights and simplicity in systems with a few users. However as systems grow in user numbers, then it usually gets difficult to manage.

Identity Based Access Control (IBAC)

RBAC (Role Based Access Control) — tries to solve the limitations of IBAC management in large systems by mimicking the real world needs more closely. Operational privileges are grouped into roles and each user is assigned a role. The role, instead of the individual, is the basis for access checks. It is often implemented in a hierarchical model, where higher level roles inherit the privileges from lower levels. RBAC sacrifices granularity for higher maintainability in systems with lots of users.

Role Based Access Control (RBAC)

Hierarchical Role Based Access Control (HRBAC)

ABAC (Attribute Based Access Control) — is an evolution of RBAC that tries to solve some shortcomings in specific situations. In systems where there are many attributes that separate access to internal resources (i.e. has the user passed some tests and been educated in the use of this part of the system etc), using the RBAC model would result in what is known as the role explosion — a need to define all the roles that separate users based on their attributes. ABAC aims to solve this problem by providing a framework for defining access rights based on the various properties of a user.

Attributes Based Access Control (ABAC)

Hope you found these detailed nuances useful? In the next section we will take a closer look at the most popular access control method of the web — RBAC.

Details of RBAC

RBAC or Role Based Access Control is an access control method where each identity is assigned a role and the roles determine what access rights the identity has. This is opposed to IBAC, where each identity has separate privilege assignment. RBAC looses some granularity compared to IBAC, however it gains better manageability in environments with large amounts of users.

RBAC is usually implemented as a Hierarchy of roles (HRBAC). This allows roles to inherit privileges from other roles, which in turn makes it easier to add new operational privileges to the whole tree.

Let’s envision an app where we have three roles: ‘Guest’, ‘Writer’, ‘Manager’. We can then illustrate the role hierarchy as follows:

If we now want to add an edit operation, which is allowed for both writer and manager, then all we have to do is extend the writer role:

The definition of roles is also a welcome feature during application development. By separating users into well defined categories beforehand we are more easily able to model the application security.

In short RBAC is the de-facto standard access control method for most web applications. Mainly because building a web app means that you expect to handle a vast amount of users — thousands, millions even billions (one can dream). Implementing IBAC in this situation would result in enormous data duplication for access rights.

Now that we are more familiar with the logic behind RBAC, we can proceed with our plan to build a RBAC module.

Ready, Set, Build

Having theoretical knowledge about access control is nice, but unless put to use, we could have spent our time watching pictures of cute kittens instead. So let’s not stop there and let’s start building

The logic of a basic RBAC model is simple — you define a number of roles and each role has privileges assigned to it. When checking for access you check if the role has access and that’s it.

So our example from before can be summed in two tables:

We can achieve this model fairly easily in JavaScript — let’s create a model of the roles and a function to check them.

let roles = {
    manager: {
        can: ['read', 'write', 'publish']
    },
    writer: {
        can: ['read', 'write']
    },
    guest: {
        can: ['read']
    }
}

function can(role, operation) {
    return roles[role] && roles[role].can.indexOf(operation) !== -1;
}

And now we have a very simple role system. Let’s give it a configurable and reusable form in the manner of a class.

class RBAC {
    constructor(roles) {
        if(typeof roles !== 'object') {
            throw new TypeError('Expected an object as input');
        }
        this.roles = roles;
    }
  
    can(role, operation) {
        return this.roles[role] && this.roles[role].can.indexOf(operation) !== -1;
    }
}

module.exports = RBAC;

This leaves us with a very simple module for defining and checking roles. Let’s not stop here — we will add hierarchy to the model so that we can manage roles more easily when adding new operations to the system.

This way there is no need to define rights to every operation for each role separately.

It’ll allow the user to represent a list of child roles, where to inherit permissions from.

let roles = {
    manager: {
        can: ['publish'],
        inherits: ['writer']
    },
    writer: {
        can: ['write'],
        inherits: ['guest']
    },
    guest: {
        can: ['read']
    }
}

And then we have to rewrite the access check functionality. In HRBAC model, the access checking begins with the current role, checks if it has access, if not then moves up to the parent and checks again. This happens until a permission is found or there are no more parents to check. So we can rewrite our checking functionality to use recursive logic:

can(role, operation) {
    // Check if role exists
    if(!this.roles[role]) {
        return false;
    }
    let $role = this.roles[role];
    // Check if this role has access
    if($role.can.indexOf(operation) !== -1) {
        return true;
    }
    // Check if there are any parents
    if(!$role.inherits || $role.inherits.length < 1) {
        return false;
    }
  
    // Check child roles until one returns true or all return false
    return $role.inherits.some(childRole => this.can(childRole, operation));
}

Now we have roles, inheritance and a function to bring it together. Almost done, but not quite there yet. There are still real use cases that we haven’t accounted for. Let me give you an example based on a blogging platform where a writer can create a blog post and then open it up for editing — should the writer role also allow to rewrite every post in the system? Probably not. We need to first check if they are the owner of the post. But how can we write that into a reusable definition — functions? To answer this, let’s allow operations to define functions that need to pass.

So to extend our existing model of roles:

let roles = {
    manager: {
        can: ['publish'],
        inherits: ['writer']
    },
    writer: {
        can: ['write', {
            name: 'edit',
            when: function (params) {
                return params.user.id === params.post.owner;
            }
        }],
        inherits: ['guest']
    },
    guest: {
        can: ['read']
    }
}

But now our check function also needs to be rewritten — we can no longer use indexOf either. Let’s create a function to normalise our input for better internal use:

class RBAC {
    constructor(opts) {
        this.init(opts);
    }
  
    init(roles) {
        if(typeof roles !== 'object') {
            throw new TypeError('Expected an object as input');
        }
    
        this.roles = roles;
        let map = {};
        Object.keys(roles).forEach(role => {
            map[role] = {
                can: {}
            };
            if(roles[role].inherits) {
                map[role].inherits = roles[role].inherits;
            }
      
            roles[role].can.forEach(operation => {
                if(typeof operation === 'string') {
                    map[role].can[operation] = 1;
                } else if(typeof operation.name === 'string'
                    && typeof operation.when === 'function') {
        
                    map[role].can[operation.name] = operation.when;
                }
                // Ignore definitions we don't understand
            });
    
        });
    
        this.roles = map;
    }
  
    // ... //
}

And now we can use the map we created in our check function:

can(role, operation, params) {
    // Check if role exists
    if(!this.roles[role]) {
        return false;
    }
    let $role = this.roles[role];
    // Check if this role has this operation
    if($role.can[operation]) {
        // Not a function so we are good
        if(typeof $role.can[operation] !== 'function') {
            return true;
        }
        // If the function check passes return true
        if($role.can[operation](params)) {
            return true;
        }
    }
  
    // Check if there are any parents
    if(!$role.inherits || $role.inherits.length < 1) {
        return false;
    }
  
    // Check child roles until one returns true or all return false
    return $role.inherits.some(childRole => this.can(childRole, operation, params));
}

Awesome! We now have RBAC class that we can use to check our defined hierarchy model. Additionally, we can also define functions to do dynamic checks for specific access:

RBAC.can('writer', 'edit', {user: user, post: post});

We are still not done. Let’s not forget that we are dealing with Node.js so synchronous solutions are not the best way to go — we need async so that we can instantiate the class with information found in the database. Or we might want our access check to look something up from the file system, other API or somewhere else. Point is — we need it.

We can provide this in two ways — with promises or callbacks. Because we want to be supportive of both styles let’s implement both. However transformation from Promise to callback is much easier than vice versa so we’ll use Promises internally.

We’ll start our update with the check function. Let’s use the Q module to provide backwards compatibility. We can just wrap the contents of our function in a promise constructor:

let Q = require('q');

// ... //

can(role, operation, params) {
    return Q.Promise((resolve, reject) => {
        // our function
        // ... //
    });
}

// ... //

We can then handle callbacks by optionally binding the handlers for our promise.

let Q = require('q');
can(role, operation, params, cb) {
    let callback = cb || () => {};
    return Q.Promise((resolvePromise, rejectPromise) => {

        // Collect resolve handling
        function resolve(value) {
            resolvePromise(result);
            callback(undefined, result);
        }

        // Collect error handling
        function reject(err) {
            rejectPromise(err);
            callback(err);
        }

        // our function
        // ... //

    });
}

We can internally handle the resolve/reject events, by specifying a callback ourselves

$role.can[operation](params, function (err, result) { 
    if(err) { 
        return reject(err); 
    } 
    if(!result) { 
        return reject(false); 
    } 
    resolve(true); 
});

And we can handle the inheritance by creating a new promise. One that resolves when any one of the child promises resolves — aka use Q.any.

return Q.any($role.inherits.map(child => this.can(child, operation, params)))
    .then(resolve, reject);

After adding some type checks, our can function could look something like this:

can(role, operation, params, cb) {

    if(typeof params === 'function') {
        cb = params;
        params = undefined;
    }

    let callback = cb || () => {};

    return Q.Promise((resolve, reject) => {

        // Collect resolve handling
        function resolve(value) {
            resolvePromise(result);
            callback(undefined, result);
        }

        // Collect error handling
        function reject(err) {
            rejectPromise(err);
            callback(err);
        }

        if (typeof role !== 'string') {
            throw new TypeError('Expected first parameter to be string : role');
        }

        if (typeof operation !== 'string') {
            throw new TypeError('Expected second parameter to be string : operation');
        }

        let $role = $this.roles[role];

        if (!$role) {
            throw new Error('Undefined role');
        }

        // IF this operation is not defined at current level try higher
        if (!$role.can[operation]) {
            // If no parents reject
            if (!$role.inherits) {
                return reject(false);
            }
            // Return if any parent resolves true or all reject
            return Q.any($role.inherits.map(parent => this.can(parent, operation, params)))
                .then(resolve, reject);
        }

        // We have the operation resolve
        if ($role.can[operation] === 1) {
            return resolve(true);
        }

        // Operation is conditional, run async function
        if (typeof $role.can[operation] === 'function') {
            $role.can[operation](params, function (err, result) {
                if(err) {
                    return reject(err);
                }
                if(!result) {
                    return reject(false);
                }
                resolve(true);
            });
            return;
        }
        // No operation reject as false
        reject(false);
    });
};

Now we are almost done. The last thing we want to support is asynchronous loading of the definitions of roles. This means we have to handle the initialisation. The easiest way to accept a function as an input that can return the configuration object after obtaining it somewhere. To do this, let’s add a check in the beginning of init function and store the resolve state in a variable:

// If opts is a function execute for async loading 
if(typeof roles === 'function') { 
  this._init = Q.nfcall(roles)
                .then(data => this.init(data)); 
  return; 
}

And add $this._inited = true before the return statement.

Now we can check at the beginning of the can function if we have managed to set up our roles and act accordingly:

// If not inited then wait until init finishes 
if(!this._inited) { 
  return this._init
             .then(() => this.can(role, operation, params, cb)); 
}

And now we are done on the functionality part.

Start Controlling Access

In the previous section we built a nice and simple access control module that I have published under the name easy-rbac. In this section we will look at how to add proper role based access control to an express application using a combination of easy-session (a session handling module I have written some time ago) and easy-rbac (the module we built, which is now integrated to easy-session).

First we will set up an express application with easy-session and a few routes for us to test our sessions:

'use strict';

let express = require('express');
let session = require('express-session');
let eSession = require('easy-session');
let cookieParser = require('cookie-parser');

let app = express();

app.use(cookieParser());
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true
}));
app.use(eSession.main(session));

// Add a path to allow easy login to any role
app.get('/login/:role', function (req, res, next) {
    req.session.login(req.params.role, function () {
        res.redirect('/');
    });
});

// A path to destroy our session
app.get('/logout', function (req, res, next) {
    req.session.logout(function () {
        res.redirect('/');
    });
});

app.get('/', function (req, res, next){
    res.send('Current role is ' + req.session.getRole());
});

app.listen(3000);

Now we have our base setup, but what if our application is expanding — we are adding functionality to read and write blog posts; everyone can read and writers can write? We could do it by checking the role like this:

// We need no authentication here
app.get('/blog', function (req, res, next) {
    res.send('Cool blog post');
});

app.get('/blog/create', function (req, res, next) {
    // Check if user is writer
    if(!req.session.hasRole('writer')) {
        res.sendStatus(403);
        return;
    }
    res.send('Blog edit');
});

This is, however, one of the most common mistakes made in implementing RBAC — looking for specific roles instead of validating operations. It is not scalable. What happens when we create new roles that are also supposed to be able to create blog posts? We would have to come back and rewrite this logic all the time. Not good.

Instead, we should be focusing on operations — roles can be added, hierarchies change, but if we always check for ‘post:create’ (the notation ‘blog:create’ has no technical implementation value, the semantics just help organise and keep a consistent naming), then we won’t have to change our code. But in order to do that, we will need to configure our integrated easy-rbac.

app.use(eSession.main(session, {
    rbac: {
        guest: {
            can: ['blog:read']
        },
        writer: {
            can: ['blog:create'],
            inherits: ['guest']
        }
    }
}));

Or if we want to store our role logic in the database layer, so that it is centralised across application instances, we can set up an async function to retrieve it:

app.use(eSession.main(session, {
    rbac: function (cb) {
        // Our async logic
        setImmediate(cb, null, {
            guest: {
                can: ['blog:read']
            },
            writer: {
                can: ['blog:create'],
                inherits: ['guest']
            }
        });
    }
}));

And now we can check for the right to create blog posts.

app.get('/blog/create', function (req, res, next) {
    // Check if user has access
    req.session.can('blog:create')
        .then(function () {
            res.send('Blog edit');
        })
        .catch(function () {
            res.sendStatus(403);
        });
});

Even better, let’s move the validation into a middleware to keep our logic clean.

app.get('/blog/create', eSession.can('blog:create'), function (req, res, next) {
    res.send('Blog edit');
});

Awesome — heading in the right direction. Let’s now set up an edit path as well. However, here we can’t just check if the user is a writer any more. I wouldn’t want some other writer to change my posts. So we are going to have to set up conditions by changing the writer’s role definition:

app.use(eSession.main(session, {
    rbac: {
        guest: {
            can: ['blog:read']
        },
        writer: {
            can: ['blog:create', {
                name: 'blog:edit',
                when: function (params, cb) {
                    //check if user is the owner
                    setImmediate(cb, null, params.user.id === params.blog.ownerId);
                }
            }],
            inherits: ['guest']
        }
    }
}));

We’ll also need the user on the session object:

app.get('/login/:role', function (req, res, next) {
    // Going to hardcode the user object
    let extend = {
        user: {
            id: 2
        }
    };
    req.session.login(req.params.role, extend, function () {
        res.redirect('/');
    });
});

And finally we need a way to look up a blog object and test if we can actually edit:

let $q = require('q');
function findBlog(id) {
    return $q({
        ownerId: parseInt(id)
    });
}

app.get('/blog/edit/:id', function (req, res, next) {
    // look for blog
    findBlog(req.params.id)
        .then(function (blog) {
            //check for access
            return req.session.can('blog:edit', {user: req.session.user, blog: blog});
        }, function (err) {
            // Handling db errors
            res.sendStatus(500);
        })
        .then(function () {
            // we have access so edit
            res.send('Editing blog');
        }, function (err) {
            // Handling auth errors
            res.sendStatus(403);
        });
});

Again we can do this with middleware

function getParams(req, res, cb) {
    findBlog(req.params.id)
        .then(function (blog) {
            cb(null, {
                user: req.session.user,
                blog: blog
            });
        }, cb);
}

app.get('/blog/edit/:id', eSession.can('blog:edit', getParams), function (req, res, next) {
    res.send('Editing blog');
});

And there we have it. A nice access control setup that we can easily reuse throughout our application.

Wrapping Up

In this post we looked at various access control methods and debunked some common misconceptions along the way. You should now know the key methodologies and how they differ.

Thank you for reading.

#JavaScript #Nodejs #Cybersecurity #Developer

Implementing Access Control in a Node.js application
1 Likes14.65 GEEK