Protecting resources & handling user authentication using Node ACL & Passport.js in NodeJs

Protecting resources & handling user authentication using Node ACL & Passport.js in NodeJs

INTRODUCTION

In the first part of this blog, I'll focus on Node ACL, later I'll give you an introduction to Passport.js, also show some code examples, and give you an explanation how to authenticate users using Passport.js.

What is Node ACL and why you should use it?

Well, if you are looking for a flexible and elegant way to protect specific resources in your application Node ACL (Access Control List for Node) is a module that can solve your problem, providing a smooth way to create roles and permissions, and assign those roles to specific users. Node ACL can be used with Redis, MongoDB and in-memory based backends. It is also applicable to other third-party backends such as knex based, firebase, and elsaticsearch. In this blog, I will show you some examples using MongoDB as a backend.

GET STARTED

You can install ACL module using npm install acl command. To get started with Node ACL, you’ll need your database instance, and then you can add ACL to your application by requiring and instantiating module. Below is an example of connecting MongoDB instance with Node ACL module.

const node_acl = require('acl');
const mongodb = require('mongodb');

In your config file, you can specify the database host, port, and the name of your database, and then create a database URL.

const config = require('./config.js');
const databaseURI = `mongodb://${config.database.host}:${config.database.port}/${config.database.name}`;

then, you can instantiate you ACL module with the following piece of code.

let acl = null;
mongodb.connect(databaseURI, (error, db) => {
   if (error) {
     throw error;
   }
   acl = new node_acl.mongodbBackend(db, '_acl');
});

After instantiating ACL module we can start with assigning permissions to roles. It can be done in multiple ways.

acl.allow('admin', '/api/users', ['GET', 'POST', 'PUT', 'DELETE'], error => {
   if (error) {
     console.log('Error while assigning permissions');
   }
   console.log('Successfully assigned permissions to admin role');
});

With these lines of code we say that the admin user is allowed to execute GET, POST, PUT, and DELETE operation on users. allow is a function that returns the promise and optionally can take callback with error parameter.

Another way to assign permissions to roles is shown in the code below:

acl.allow([
    {
      roles: ['user'],
      allows: [
        {
          resources: ['/api/events', '/api/categories'],
          permissions: ['get', 'post', 'put', 'delete'],
        },
      ],
    },
    {
      roles: ['admin'],
      allows: [
        {
          resources: ['/api/users'],
          permissions: ['get', 'post', 'put', 'delete'],
        },
      ],
    },
]);

By looking on the code above, you could tell that the user has permissions over more resources than admin which does not make sense, but in order to not duplicate any code ACL offers a nice way to deal with duplication of code.

acl.addRoleParents('admin', 'user');

addRoleParents is a function that you can use to tell that every admin is allowed to do what users can do. If you want to remove role parents, you can do that with removeRoleParents function and it should look like this:

acl.removeRoleParents('admin', 'user', error => {
     if (error) {
       console.log('An error occured while removing role parents ', error);
     } else {
       console.log('Role parents successfully removed.');
     }
});

After assigning various permissions to our roles, we should now assign roles to users, and it can be done with the following piece of code.

user
    .find()
    .exec()
    .then(users => {
      users.forEach(user => {
        acl.addUserRoles(user._id.toString(), user.role, err => {
          if (err) {
            console.log(err);
          }
          console.log('Added ', user.role, ' role to user ', user.firstName, ' with id ', user._id);
        });
      });
  });
Output:
//    Added user role to user Pero with id 5b263f5319703219ab25fb1b
//    Added admin role to user Jovica with id 5b1fa76f5acb3a07897458b2

With this code, we are going through the list of the users from our database and assigning roles to every user.

Afterwards, Node ACL gives us the possibility to check what we did so far. If we want to check which users we have given access to resources, and what are those resources, we can do it easily with allowedPermissions function.

user
    .find()
    .exec()
    .then(users => {
      users.forEach(user => {
        acl.allowedPermissions(user._id.toString(), ['/api/users', '/api/events', '/api/categories'], (_, permissions) => {
          console.log(user.firstName, ' has role ', user.role);
          console.log(permissions);
        });
      });
  });

acl.allowedPermissions is a function that returns all permissions that we allowed to a specific user to access given resources. As the first parameter, it takes user id as a string, and the second parameter is an array of resources that we defined. The last parameter (which is optional) is a callback which you can use to show permissions.

Output:

Jovica   admin
{ '/api/categories': [ 'delete', 'get', 'post', 'put' ],
  '/api/events': [ 'delete', 'get', 'post', 'put' ],
  '/api/users': [ 'delete', 'get', 'post', 'put' ] }

Pero   user
{ '/api/categories': [ 'delete', 'get', 'post', 'put' ],
  '/api/users': [],
  '/api/events': [ 'delete', 'get', 'post', 'put' ] }

From the output we can see that Jovica admin has permissions to all categories, events and users resources, but Pero, who is a user, has only permissions to access categories and events resources, just like we specified with acl.allow() function.

CREATING ACL MIDDLEWARE

All lines of code that we have written so far are part of the Node ACL configuration. It's really simple, right? :)

The following part is a place where the fun begins. What we are going to do next is to apply ACL middleware to routes and then test it to see if we've configured everything in the right way.

const NUM_PATH_COMPONENTS = 2;

router.get('/api/users/', checkForPermissions(), userController.getAllUsers);

function checkForPermissions() {
    return acl.middleware(NUM_PATH_COMPONENTS, getUserId);
}

function getUserId(req) {
    if (req.user) {
      return req.session.passport.user;
    }
}

checkForPermissions is a function that calls ACL middleware which should protect the resource that we specify, (It is important to use the same name of the resource as we specified in acl.allow() function because otherwise ACL middleware will not operate in the right way), by taking user id from the session and checking the permission for given req.method. In my case, I'm using getUserId function to get userId, from the session object. NUM_PATH_COMPONENTS is a constant that you can use to set a number of components in the URL to be considered as part of the URL. e.g. if I set NUM_PATH_COMPONENTS to be 1, then ACL middleware will only take /api as the part of the URL.

If everything goes smoothly, we should get all users. But if you try to get users by sending a request with the user that isn’t an admin, then you’ll get the following error:

error: {
  message: 'Insufficient permissions to access resource'
}

Another way to achieve the same functionality is by using acl.isAllowed(userId, resource, permissions, (error, allowed)) function which will check if the user is allowed to access resource for the given permissions.

function checkPermissions(req, res, next) {
   if (req.user) {
     acl.isAllowed(
       req.session.passport.user.toString(),
       req.url, req.method.toLowerCase(), (error, allowed) => {
         if (allowed) {
           console.log('Authorization passed');
           next();
         } else {
           console.log('Authorization failed')
           res.send({ message: 'Insufficient permissions to access resource' })
         }
       });
   } else {
     res.send({ message: 'User not found' })
   }
}

This function will first check if the user exists and then it will use acl.isAllowed function to protect the resource. As the first parameter isAllowed function takes userId, the second parameter is the resource that it should protect, then the third parameter is the requested method, and as the last parameter, it takes a callback function which takes two parameters: error and allowed.

If an allowed parameter is true then authorization has passed, and if it is not authorization has failed. Here we are done with Node ACL, and now I'll focus on Passport.js.

INTRODUCTION TO Passport.js

Passport.js is the authentication middleware for Node.js applications which can be used in Express-based applications. One of the most remarkable things about this middleware is its simplicity regarding the integration into the application. It will be followed up with the code examples within the text.

A significant fact that makes Passport.js great is that it provides us with 500+ strategies for request authentications, especially when we know that each of these strategies is a separate module that you can integrate into your application simply by installing their npm package. These strategies support traditional username and password authentication, and since today's modern web applications have demand for authentication with Facebook, Twitter or sign-on with Google, Passport.js also provides strategies for this kind of authentications. To cut the story short, let's see some coding examples.

INSTALLING & AUTHENTICATION

To install Passport.js you'll need just one command:

npm install passport

Let's see how simple traditional username and password authentication looks alike. In Passport.js for this kind of authentication we use "LocalStrategy", and to use this strategy we simply write one line of code.

let LocalStrategy = require(passport-local).Strategy;

The next thing that has to be done is the configuration of our Passport.js. I have created a separate file for my configuration that looks like this:

module.exports = passport => {

  passport.serializeUser((user, done) => {
    done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    User.findById(id, (err, user) => {
      done(err, user);
    });
  });

  passport.use(
    'local',
    new LocalStrategy(
      {
        usernameField: 'email',
        passwordField: 'password',
        passReqToCallback: true,
      },
      (req, email, password, done) => {
        User.findOne({ email: email }, (err, user) => {

          if (err) {
            return done(err);
          }

          if (!user) {
            return done(null, false, req.flash('loginMessage', 'User not found.'));
          }

          if (!user.validPassword(password)) {
            return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.'));
          }

          return done(null, user);
        });
      }
    )
  );
};

The code that you've seen is actually all you need for basic username/password configuration, and I will walk you through each line of code.

(De)serialization and Local Strategy

The first thing that you can see is serialization and deserialization of the user. So, those first two blocks of code are examples of how Passport.js will serialize user ID and keep the amount of data stored in session small. When the next request is received that user ID will be used to find the user which will be restored to the user object inside the request.

Next step: we are telling Passport.js that we want to use Local Strategy. The Local Strategy uses username and password by default, and since my login form contains e-mail instead of a username, I have replaced username field with the email field, and as the last property comes passReqToCallback which I set to true, to allow me to pass back the entire request to the callback.

After defining our Local Strategy, we are checking if the user trying to login already exists in the database. If some server exception occurs we simply pass error as the argument to done callback. If the user was not found that means that the user has entered the wrong email address or has not yet been registered. So we pass null as the first argument and false as the second argument to indicate that user authentication is not successful, and the last argument is flash message that can be useful for displaying a message to users with the right cause of authentication failure. If the user is found but the password is wrong then we again return done callback with false instead of user object to indicate that authentication is not successful and with a message that clearly describes authentication failure.

At the end, if the user enters the right email and password then we simply pass null and user object to done callback to indicate that authentication is successful. And that is all that you need to configure your Local Strategy.

The last piece of code that I want to show you is code that you need after the user submits login form.

app.post('/login', passport.authenticate('local', { 
      successRedirect: '/home',
      failureRedirect: '/login',
      failureFlash: true })
);

Here, after the form is submitted we are using authenticate() method from Passport.js with Local Strategy to handle login requests. Also, we are setting successRedirect and failureRedirect options to instruct Passport.js to witch route application should go if authentication is successful or unsuccessful, and the last thing is failureFlash option that we set to true to tell Passport.js that we want to have flash messages that we set in done callbacks in our Passport.js configuration.

CONCLUSION

To summarize:

In the first part of this blog we used Node ACL module to protect resources in our application and as you can see ACL is really simple and easy to use. The important thing that we’ve seen is that it provides you a nice and elegant way to protect resources in your application. After that we focused on Passport.js, I showed you only one strategy for user authentication, and there is 500+ more. I hope this blog will help you if you're planning to integrate Node ACL and Passport.js into your project.