Role-based Authentication in Node Js

Role-based Authentication in Node Js

Part 1:What is role and rights?

Role rights implementation is an important part of any software. Role is a position of responsibility and every responsibility enjoys some rights given to them. There may be some common rights between a few roles and some rights may strictly belong to a specific role.

Rights are Urls that a role is authorized to access. It is thus necessary to create a collection in DB storing information of rights to a role. We have role collection schema as

const mongoose = require('mongoose');  
const Schema = mongoose.Schema;  
const RoleSchema = new Schema({  
roleId:{  
type:String,  
unique:true,  
required:[true,"Role Id required"]  
},  
type:{  
type:String,  
unique:true,  
required:[true,"Role type is required"]  
},  
rights:[{  
name: String,  
path: String,  
url: String  
}]  
});  
module.exports = Role = mongoose.model('role',RoleSchema);

Now, remember every role that is supposed to exist is in Role collection and of above schema type.

In schema rights array of the object we see the object has keys:

  • name(for the name of URL like “set-username”)
  • path(for base path hit “/users/”)
  • url(requested URL or complete path “/users/set-username”)

Thus if a user with role user has right to change username then he can hit URL /users/set-username.However a wanderer will not be able to access this url. A higher role like admin & superadmin should logically have access to all lower role rights(URLs).

Role in real application are:-

  1. Wanderer (Someone who is just visiting our site. He should be able to access all public routes. Simple URLs/Public URLs accessible to all thus need not make a separate role for this as it is not any authenticated right.)
  2. Guest (Someone who has registered but not verified say email not verified).
  3. User (Someone who has his verified email)
  4. Admin (Made an Admin by SuperAdmin after verifying.he enjoy most of the rights)
  5. Superadmin (Master of application.He enjoy some more sophisticated rights.More rights then admin)

Till now we have understood what exactly is right and how it is mapped to a role.

Part 1.5: Registered Urls/Config Urls

Here we have a file called registeredUrls.js which is like:

module.exports = {  
    // globally accessible 
    simple: {  
        "/":[""],  
        '/users/':["login","register"],  
    },  
    auth:{  
        //admin level enpoint

        '/admin/':   ['load-users' , 'set-new-password', 'delete-user'],  
        '/teacher/':  ["add-teacher", "delete-teacher", "edit-teacher"],  
        '/student/':  [
            "add-student", 
            "delete-student", 
            "edit-student", 
            "test-result"
        ],

       // common user enpoint

        '/test/':  ["view-test", "submit-test"],  
        '/profile/': [
            'change-username', 
            'update-profile-data',  
            'set-new-password', 
            'upload-pic', 
            'update-social-links'
         ],  
        '/teacher/':['load-teacher'],  
        '/student/':['load-student']
}

Similarly confgUrls.js

const configUrls= {  
    '/roles/': [
        'get-rights', 
        'create', 
        'update-rights', 
        'load', 
        'delete', 
        'assign'
     ]  
}  
module.exports = configUrls;

Part 2:Creating SuperAdmin

This is the most essential part of the application. Whenever the server starts for the first time or restarts/reboots this step occurs. In config/init.js follow the procedure:

  1. Load All simple URLs(public) and Auth Urls(admin & users) & super-admin-specific URLs into superAdminRights[].
  2. Then run a function to create a user with role superadmin if doesn’t exist.
  3. Get a Role of type:”superadmin” if found:replace its rights with new rights(superAdminRights).else:create Role of type :”superadmin” and then fill its rights(superAdminRights).

At the end of this function call, we are always sure that we have a superadmin in application with all its sophisticated URLs/rights initialized.

Part 3:Super-Admin-Specific-URLs

These are rights that are enjoyed by super admin only and must be maintained in a separate file in parallel to the registered URL file. These include URL rights which map routes used only by superadmin. Here we have routes to create role, load roles, get-rights for a roleId, update-rights for roleId/role type, assign-role to a user, delete a role.

For each user in code, we need to change their role from guest to the user(say after email verification). Or guest/user to admin by superadmin using assign-role URL. Then updating admin rights using route update-rights.

The process ensures that every role has A collection Document and filled rights there.

Part 4:Authenticator Middleware

This heart of our RBACS logic. Here we use a middleware which follows the process:

// get all the URLs/endpoints in the system


const URLS = require("./registeredUrls");
// special endpoints
const CONFIG_URLS = require("./configUrls");

// create array of all endpoints and separate them by auth flows

// simple urls doesn't need any auth flow
const SIMPLE_URLS = [];

// AUTH_URL and SUPER_URL need auth flow
const AUTH_URLS = [];
const SUPER_URLS = [];

// the data-structure is { [rootURL]: [...subURLs..] }

// traverse all registered paths of simple URLs
// and make complete paths i.e `rootURL/subURL`
for (const rootURL in URLS.simple) {
  const subURLs = URLS.simple[rootURL];
  for (const subURL of subURLs) {
    // register all these in SIMPLE_URLS
      SIMPLE_URLS.push([rootURL, subURL].join("/"));
  }
}

// same with AUTH...register as AUTH_URLS
for (const rootURL in URLS.auth) {
  const subURLs = URLS.auth[rootURL];
  for (const subURL of subURLs) {
      AUTH_URLS.push([rootURL, subURL].join("/"));
  }
}

// same with CONFIG_URLS...register as SUPER_URLS
for (const rootURL in CONFIG_URLS) {
  const subURLs = CONFIG_URLS[rootURL];
  for (const subURL of subURLs) {
      SUPER_URLS.push([rootURL, subURL].join("/"));
      // push super URLS in AUTH_URLS also as they need auth flow
      AUTH_URLS.push([rootURL, subURL].join("/"));
  }
}


// By now we have an array of URLs
// 1. Simple URLs don't need authentication flow SIMPLE_URLS
// 2. Authentication required URLs need auth-token
// 3. Config URLs are the highest roles URLs typically super admin
// and have the same flow as Auth URL

// in the node.js middleware callback
const middleware = (req, res, next) => {
  // URL or endpoint or path requested
  const reqURL = req.url;

  // check where URL is
  const isAuthURL = AUTH_URLS.includes(reqURL);
  const isSimpleURL = SIMPLE_URLS.includes(reqURL);

  // auth URLs need auth flows

  if (isAuthURL) {

    // get token from header
    const token = getToken(req);

    // validate 
    const isValidJWTToken = validateJWT(token);
    if (!token || !isValidJWTToken) {
      // send failed authentication response
      // !token missing token required login
      // !isValidJWTToken token is invalid or expired
      return;
    }

   // token is valid but we have to check if the session exists in DB
   const user_session = getUserSessionData(token);

   // If no session in DB means this token may be mischievously generated
    if (!user_session) {
      // the user token might be compromised
      return;
    } 

   // now check if user_session.rights [] has requested URL
   // if requested URL is assigned is in array means 
   // it is assigned that right/path 

     const hasRightToPath = user_session.rights.includes(reqURL);
    if (!hasRightToPath) {
      // user doesn't have access to this path
      return;
    }
    // user has right to this path/endpoint/URL so let them do the thing
    return next();
  }

  if (isSimpleURL) {
    return next(); // simple URL doesn't need any flow simply pass it down
  }

  // if it matched none means it isn't a registered path in the system
  return;
}

Did you find this article valuable?

Support Aniket Jha by becoming a sponsor. Any amount is appreciated!