The Kick Ass Guide to Developing Access Control Systems for Nodejs Webapps


All men may be born equal but if you are developing web apps, you certainly do not want to treat all your users the same. You may have users who have paid to access premium features of your service or perhaps, certain users are designated as administrators who wield awesome powers that would be devastating in the hands of mere mortals. In almost all but the simplest webapps, it is crucial to be able to differentiate between classes of users and treat them accordingly.

The simplest way to perform access control, for most general purposes, is to implement a role-based access control system. In a role based system, access to resources is determined by (you guessed it) roles. For example, your system could enable all users with the role "Paid Subscriber" to view content otherwise hidden behind a paywall or only allow users with "Moderator" roles to approve comments before making them public. The advantage of the role based system is that once you have the roles, resources and permissions set up, you simply have to assign (or remove) users from roles as required. This system scales easily as your userbase grows since the number of roles will likely remain quite small. Also, with this system, users can have multiple roles assigned and the system will automatically grant access if any of their roles is relevant to the request.

Dependencies
node_acl module
The node_acl module manages the assignment of roles and verification of user access

node_redis module
The node_redis module persists the roles and user assignment data in the redis in-memory database

Stop! in the name of node?
To illustrate how a role-based access control system works, let us design an access control system for a simple forum. The first step is to break out pen and paper and layout your resources, roles and permissions.

The resources, in this case, are simply the "posts" made on the forum. The permissions would be to "view", "create", "edit" and "delete". Meaning, users could only possibly view existing posts, create new posts. edit existing posts or delete existing posts. The roles would be "guest" (users without accounts), "registered users" (users who have signed up) and "administrators" (small group of super-users with special capabilities). Presumably, we would like all users to be able to "view" posts but only registered users i.e. people with accounts to be able to "create" new posts and only administrators would have the ability to "edit" or "delete" posts.

Now, let us get to coding

assigning permissions to roles

    
        // create a redis backend
        var client = require('redis').createClient(6379, '127.0.0.1', {no_ready_check: true});

        // initialize acl system storing data in the redis backend
        var acl = require('acl');
        acl = new acl(new acl.redisBackend(client, "acl_"));

        /* now assign permissions to roles */

        // allow guests to view posts
        acl.allow("guest", "post", "view");

        // allow registered users to view and create posts
        acl.allow("registered users", "post", ["view", "create"]);

        // allow administrators to perform any action on posts
        acl.allow("administrator", "post", "*");
    



So far, we have assigned various permissions to our roles. We now need to be able to assign roles to the users.

assign roles to users

    
        /*
         * represent users by object
         * User = {id: <user id>, name: <user name> }
         * 
         * with default value of User.id = 0 (i.e. unregistered users)
         * /

        // make user "Alice" {id: 1, name: "Alice"} an administrator
        acl.addUserRoles(1, "administrator, callback);

        // make user "Bob" {id: 2, name "Bob"} a registered user
        acl.addUserRoles(2, "registered user", callback);

        // make all users that are not signed in (i.e. id=0) guests
        acl.addUserRoles(0, "guest", callback);
    



Great, now we have created various roles, assigned permissions to them and then assigned roles to our users. The last piece of the puzzle is to check if a particular user has the right to perform an action on a specified resource.

There are two circumstances in which we would want to check a user's access rights. Either the user is trying to access a route or the user is executing a function. In the former case, the check needs to be implemented as an express middleware whereas in the latter case, it can be a simple function that returns true/false. To maintain DRY (Do Not Repeat Yourself) standards, we can create a single function that operates as both a regular function but becomes a middleware when required.

    
    /*
     * checkPermission
     * checks if a user has permission to perform an action on a specified resource
     *  
     *  @param {string} resource - resource being accessed
     *  @param {string/array} action - action(s) being performed on the resource
     *  @param {object} req - express request object
     *  @param {object} res - express response object
     *  @param {object} next - express middleware next object
     */

    function checkPermission(resource, action){
        var middleware = false;  // start out assuming this is not a middleware call

        return function(req, res, next){
            // check if this is a middleware call
            if(next){
                // only middleware calls would have the "next" argument
                middleware = true;  
            }

            var uid = req.session.user.id;  // get user id property from express request

            // perform permissions check
            acl.isAllowed(uid, resource, action, function(err, result){
                // return results in the appropriate way
                select (middleware){
                    case true;
                        if(result){
                            // user has access rights, proceed to allow access to the route
                            next();
                        } else {
                            // user access denied
                            var checkError = new Error("user does not have permission to perform this action on this resource");
                            next(checkError);  // stop access to route
                        }
                        return;
                        break;
                    case false:
                        if(result){
                            // user has access rights
                            return true;
                        } else {
                            // user access denied
                            return false;
                        }
                        break;
                }
            });
        }
    }


    /*
     * now let's show some examples of how checkPermission would be used
     */

    /*
     * example 1: function editContent is used to edit content.
     * When this function is called, the first thing it does is to 
     * check if the user has permission to perform this edit
     */

    function editContent(post, req, res){
        if(!checkPermission(post, "edit")(req, res)){
            return new Error("sorry you don't have permission to edit this post");
        }

        // user has required access permission so it is ok
        // to go ahead and run code to perform the content edit
        // ....
    }

    /*
     * example 2: route "/topsecret" provides access to classified documents.  checkPermission can be used to verify a user has the proper access rights
     */

     app.get("/topsecret", checkPermission("classified documents", "view"), function(req, res){
        res.send("if you are reading this, you have proper clearance);
     });
    



Open sesame
Access control can be a tricky thing to manage. As the last example above shows, access control can be quite important to get right. Despite the fact that most of us will not be writing code to protect state secrets (and if you are, you really should not be relying on just this simple tutorial), nonetheless our users are often entrusting us with their documents, contact information and lots of other personal information that need to be protected. Also, and just as important, access control protects you from your users. The last thing you need is to give Johnny McTroll the same rights as an administrator so he can harras other legitimate users and wipe out content from your webapp at will.

The most important step in creating an access control system is the first step; taking the time to think about all the resources and permissions and deciding how you would like your users to interact with the system. Before you write a line of code, map it out on paper to ensure your access control plan makes sense. Ask yourself if it is possible for a user to acquire access rights to a resource through an unexpected means. For instance, many websites limit the resources available to a user who has registered an account but has not yet verified their email address. Let's say this user is assigned a role "unverified" and does not have the permission to "create" a "post" resource. However, many sites also grant special rights to users who have held accounts for a certain amount of time. Let's say these users are assigned to the role "oldtimer". If an "unverified user" is also granted the role "oldtimer" (usually done automatically in some cron batch job) and the role "oldtimer" has any permissions that exceed that of the "unverified" role, all of a sudden your unverified user has been promoted to a level of access you did not expect them to have.

Another, surprisingly common, way access control is breached is when the check is simply not applied. For instance, consider a scenario where only users designated as "executive assistants" are allowed to manage "reservations" on "meeting rooms" (assuming this was a facility management app). A fairly common mistake would be apply the access control check only on the page that displays the room reservation form and not on the endpoint of the form as well. Well, what is the problem you ask? After all, if the user cannot see the form then they obviously cannot execute the form action and so the second access control check is a redundant waste of processing cycles. True, but forms tend to move around over time in a web app. If a design change to the webapp embeds the form in another page which does not have the same permission settings and you did not protect the form endpoint, then the floodgates are suddenly flung open and chaos will reign (trust me, I have lived this exact scenario and it isn't pretty). Access control is one of those features that needs to be resilient. It needs to work now and also in the future after you have made unanticipated changes to your webapp.

Unfortuantely, access control is quite difficult to test in practice. This is because it is generally difficult to test a web app user interface and also, the permutations, for anything beyond a simple app, are usually too many to fully consider. Often, the first clue that you screwed up your access control is a user email saying "um, something seems wrong with my account, it looks like I can see... ". This is why it pays to spend the time beforehand planning your access control scheme and practising defence in depth.

Login or register an account to leave a comment

Comments


Sign up to receive more nodejs tips like this