DEV Community

Callis Ezenwaka
Callis Ezenwaka

Posted on • Updated on

Use Firebase Auth to Manage User Permissions and Enforce Principle of Least Privilege on API Endpoints. Part 1.

The principle of least privilege is a foundational component of zero trust frameworks. These sets of frameworks advocate that organizations should not automatically trust activities within or outside their infrastructure perimeters.

It demands that organizations setup and deploy stringent authentication and authorization protocols and grant only the necessary access needed for executing a task to devices that connect to either their remote or on-premises systems.

Many organizations are accelerating their digital transformation strategies, thus shifting from traditional perimeter security approaches to the Zero Trust framework to protect their most sensitive networks.

This piece will focus on fine-grain permissions of server endpoints using Firebase, an app development platform that helps you build and grow apps and games users love. We have Node.js®, an open-source, cross-platform JavaScript runtime environment. Check out the part 2 here.

One might ask, why is the Principle of Least Privilege important? Some answers are provided below:

  1. It reduces the cyber-attack surface.
  2. It stops the spread of malware.
  3. It improves end-user productivity.
  4. It helps streamline compliance and audits.

A simple project setup will be used to make the tutorial as basic as possible. However, the same approach could be extended and applied to more complex Enterprise SaaS applications. The code tree is as shown below:

.
├── README.md
├── controllers
│   └── user.js
├── database
│   └── index.js
├── index.js
├── lib
│   └── auth.js
├── migrate.js
├── package-lock.json
├── package.json
├── permission.json
└── routes
    └── user.js
Enter fullscreen mode Exit fullscreen mode

Here, we have four directories viz: controllers, routes, databaseand lib. As already mentioned, the setup is simplified by using a single user route. The controller directory has the user.js file with the endpoint callback functions. While the routes directory has the various endpoints to perform CRUD operations.

The lib directory has the auth.js file for the authentication related functions. Other files are the index.js file, which as we know is the entry point for the project. The permission.json is another important file as it contains the firebase service account settings.

The database has the mock data with a user role. The user role is assigned to a user using the setCustomUserClaims of the firebase getAuth method. The mock data consist of user object with the following schema:

{
    id: '1',
    username: 'janedoe',
    displayName: 'Jane Doe',
    email: 'jane.doe@permission.com',
    password: 'password',
    phoneNumber: '+2348030004001',
    photoURL: 'http://avatar.png',
    emailVerified: true,
    disabled: false,
    role: {
      name: "User",
      entity: "isUser",
      permissions: [
        { name: "create", value: false, },
        { name: "readAll", value: false },
        { name: "read", value: true, },
        { name: "update", value: true, },
        { name: "delete", value: false, },
      ],
    },
    created_at: 1673431871000,
    updated_at: 1673431871000,
  }
Enter fullscreen mode Exit fullscreen mode

As we can see, the role variable has the name, entity, permissions etc. The permissions variable is also an array of key-value pairs of name and value, the fine-grained access permissions. The name is the intended action while the value is the privilege to perform such action.

Then, there is the migration.js file which is used to seed data to firebase authentication table. And finally, the package.json file, which has the script object for starting and migrating the mock data. To migrate the mock data, run the command npm run migration up. (N.B: Make sure that you do not skip the last up word in the command).

  "scripts": {
    "start": "node index.js",
    "dev": "nodemon -r dotenv/config index.js",
    "migration": "node migrate.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
Enter fullscreen mode Exit fullscreen mode

The project set has five endpoints: getUsers, getUser, addUser, updateUser, and deleteUser. Each of these endpoints has a corresponding action: readAll, read, create, update and delete respectively. As previously stated, the simplicity of this implementation is for demonstration purposes.

The auth.js file has two major function definitions. The isAuthenticated function for authentication and the isUser function for authorization. This role is added to the account custom claim during the account creation step.

/**
 * [START CHECK USER]
 * @param {object} req Express request context.
 * @param {object} res Express response context.
 * @param {object} next Express next context.
 */
exports.isUser = (action) => (req, res, next) => {
    getAuthToken(req, res, async () => {
        try {
            // TODO: get and verify authToken
            const { authToken } = req;
            const userInfo = await getAuth().verifyIdToken(authToken);
            // TODO: user has permission required to perform action on any API method
            const allow = userInfo.permissions.some(permission => permission.name === action && permission.value === true);
            // TODO: user not allowed to take action
            if (!allow) return res.status(403).json('Forbidden access!!');
            // TODO: user allowed to take action
            req.user = userInfo;
            return next();
        }
        catch (error) {
            return res.status(401).json('Unauthorized access!');
        }
    });
}
// [END CHECK USER]
Enter fullscreen mode Exit fullscreen mode

This, I am certain, is very explanatory. The action parameter is used to determine the intended task and if a user making a request or expecting a response has the right permission. These permissions and the corresponding authentication function is used to authenticate and authorize requests and responses.

The isAuthenticated is passed as application middleware within index.js to authenticate requests as below:

// Verify request middleware
app.use('/', isAuthenticated);
// Private routes
app.use('/api/v1/user', user);
Enter fullscreen mode Exit fullscreen mode

While the isUser role with its associated permission is passed before the callback function as a middleware on every route endpoint to authorize user actions.

'use strict';

// import packages and dependencies
const user = require('../controllers/user');
const { isUser } = require('../lib/auth');
const express = require('express');
const router = express();

router.get('/', isUser('readAll'), user.getUsers); 

router.get('/:id', isUser('read'), user.getUser);

router.post('/', isUser('create'), user.addUser);

router.put('/', isUser('update'), user.updateUser);

router.delete('/:id', isUser('delete'), user.deleteUser);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

That is much about setting up the Principle of Least Privilege on API Endpoints on the server. Next, we will cover how to hook it up with client to authenticate and authorize a user to perform an action. The repository for this tutorial is on GitHub.

If you like the article, do like and share with friends.

Reference:
https://www.cyberark.com/what-is/least-privilege/

Top comments (0)