Use Firebase Auth to Manage User Permissions and Enforce Principle of Least Privilege on API Endpoints. Part 1.
Tags: firebase, javascript, security, api
Published: 2023-04-10 10:35:47

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](https://firebase.google.com/), an app development platform that helps you build and grow apps and games users love. We have [Node.js®](https://nodejs.org/en), an open-source, cross-platform JavaScript runtime environment. Check out the [part 2](https://dev.to/callezenwaka/user-permissions-and-the-principle-of-least-privilege-on-api-endpoints-using-firebase-part-2-29hn) 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
```

Here, we have four directories viz: `controllers`, `routes`, `database `and `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,
  }
```
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"
  },
```

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]
```

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);
```

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;
```

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](https://github.com/callezenwaka/demos/tree/master/node-server).

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

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