Adding Authorization to Your Node.js Application Using Cerbos
Tags: node, authorization, cerbos, authz
Published: 2021-12-06 10:47:26

Authorization is critical to web applications. It grants the correct users access to sections of your web application on the basis of their roles and permissions. In a simple application, adding in-app authorization to your application is relatively straightforward. But with complex applications comes a need to create different roles and permissions, which can become difficult to manage. 

In this tutorial, you’ll learn how to use [Cerbos](https://cerbos.dev/) to add authorization to a Node.js web application, simplifying the authorization process as a result.

## Setting Up the Node.js Application

Before we get started with Cerbos, you’ll need to create a new Node.js application (or use an existing one). Let’s set up a blog post Node.js application as our example. 

### Defining User Permissions

The blog post application will contain two roles: **member** and **moderator**. 

The member role will have the following permissions:

– create a new blog post
– update blog posts created by the member
– delete blog posts created by the member
– view all blog posts created by all members
– view a single blog post created by any member

The moderator role will have the following permissions:

 – view all blog posts created by all members
 – view a single blog post created by any member
 – disable and enable a malicious post

> Members and moderators cannot perform any action if they are disabled.

### Creating the Application

#### Step 1

Launch your terminal or command-line tool and create a directory for the new application:
 
```
mkdir blogpost
```

#### Step 2

Move into the blog post directory and run the command below—a `package.json` file will be created:

```
npm init -y
```

#### Step 3

Open the `package.json` file and paste the following:

```json
{
	"name": "blogpost",
	"version": "1.0.0",
	"description": "",
	"main": "index.js",
	"scripts": {
		"start": "nodemon index.js",
		"test": "mocha --exit --recursive test/**/*.js"
	},
	"keywords": [],
	"author": "",
	"license": "ISC",
	"dependencies": {
		"cerbos": "0.0.3",
		"express": "^4.17.1"
	},
	"devDependencies": {
		"chai": "^4.3.4",
		"chai-http": "^4.3.0",
		"mocha": "^9.0.3",
		"nodemon": "^2.0.12"
	}
}
```

Two main packages are in the dependencies section of the`package.json`—Cerbos and Express:

 - Cerbos is the authorization package responsible for creating roles and permissions.
 - Express is a Node.js framework used to set up and create faster server-side applications. 

In the devDependencies, there are four packages: Chai, Chai HTTP, Mocha, and Nodemon. Chai, Chai HTTP, and Mocha are used to run automated test scripts during and after development. Nodemon is used to ensure the application server is restarted whenever a change is made to any file during development.

#### Step 4

Run `npm install` to install the packages in the `package.json`.

#### Step 5

Create the following files:

  – `index.js`, which contains the base configuration of the demo application.
  – `routes.js`, which contains all the routes needed in the demo application.
  – `db.js`, which exports the demo database. For the sake of this demo, you will be using an array to store the data—you can use any database system you desire.
  – `authorization.js`, which contains the Cerbos authorization logic.

  ```
    touch index.js routes.js db.js authorization.js
  ```

Then, paste the following codes in the respective files:

```javascript
//index.js

const express = require("express");
const router = require("./routes");
const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use("/posts", router);
app.use((error, req, res, next) => {
  res.status(400).json({
    code: 400,
    message: error.stack,
  });
});

app.listen(3000, () => {
  console.log("App listening on port 3000!");
});

module.exports = app;
```

```javascript
//routes.js

const express = require("express");
const router = express.Router();
const db = require("./db");
const authorization = require("./authorization");

const checkPostExistAndGet = (id) => {
  const getPost = db.posts.find((item) => item.id === Number(id));
  if (!getPost) throw new Error("Post doesn't exist");
  return getPost;
};

router.post("/", async (req, res, next) => {
  try {
    const { title, content } = req.body;
    const { user_id: userId } = req.headers;

    await authorization(userId, "create", req.body);

    const newData = {
      id: Math.floor(Math.random() * 999999 + 1),
      title,
      content,
      userId: Number(userId),
      flagged: false,
    };
    db.posts.push(newData);

    res.status(201).json({
      code: 201,
      data: newData,
      message: "Post created successfully",
    });
  } catch (error) {
    next(error);
  }
});

router.get("/", async (req, res, next) => {
  try {
    const getPosts = db.posts.filter((item) => item.flagged === false);

    const { user_id: userId } = req.headers;
    await authorization(userId, "view:all");

    res.json({
      code: 200,
      data: getPosts,
      message: "All posts fetched successfully",
    });
  } catch (error) {
    next(error);
  }
});

router.get("/:id", async (req, res, next) => {
  try {
    const getPost = db.posts.find(
      (item) => item.flagged === false && item.id === Number(req.params.id)
    );

    const { user_id: userId } = req.headers;
    await authorization(userId, "view:single");

    res.json({
      code: 200,
      data: getPost,
      message: "Post fetched successfully",
    });
  } catch (error) {
    next(error);
  }
});

router.patch("/:id", async (req, res, next) => {
  try {
    const { title, content } = req.body;
    let updatedContent = { title, content };
    const { user_id: userId } = req.headers;

    const postId = req.params.id;
    checkPostExistAndGet(postId);

    const tempUpdatedPosts = db.posts.map((item) => {
      if (item.id === Number(postId)) {
        updatedContent = {
          ...item,
          ...updatedContent,
        };
        return updatedContent;
      }
      return item;
    });

    await authorization(userId, "update", updatedContent);

    db.posts = tempUpdatedPosts;

    res.json({
      code: 200,
      data: updatedContent,
      message: "Post updated successfully",
    });
  } catch (error) {
    next(error);
  }
});

router.delete("/:id", async (req, res, next) => {
  try {
    const { user_id: userId } = req.headers;
    const postId = req.params.id;
    const post = checkPostExistAndGet(postId);

    const allPosts = db.posts.filter(
      (item) => item.flagged === false && item.id !== Number(postId)
    );

    await authorization(userId, "delete", post);

    db.posts = allPosts;

    res.json({
      code: 200,
      message: "Post deleted successfully",
    });
  } catch (error) {
    next(error);
  }
});

router.post("/flag/:id", async (req, res, next) => {
  try {
    let flaggedContent = {
      flagged: req.body.flag,
    };
    const { user_id: userId } = req.headers;

    const postId = req.params.id;
    checkPostExistAndGet(postId);

    const tempUpdatedPosts = db.posts.map((item) => {
      if (item.id === Number(postId)) {
        flaggedContent = {
          ...item,
          ...flaggedContent,
        };
        return flaggedContent;
      }
      return item;
    });

    await authorization(userId, "flag", flaggedContent);

    db.posts = tempUpdatedPosts;

    res.json({
      code: 200,
      data: flaggedContent,
      message: `Post ${req.body.flag ? "flagged" : "unflagged"} successfully`,
    });
  } catch (error) {
    next(error);
  }
});

module.exports = router;
```

```javascript
//db.js

const db = {
  users: [
    {
      id: 1,
      name: "John Doe",
      role: "member",
      blocked: false,
    },
    {
      id: 2,
      name: "Snow Mountain",
      role: "member",
      blocked: false,
    },
    {
      id: 3,
      name: "David Woods",
      role: "member",
      blocked: true,
    },
    {
      id: 4,
      name: "Maria Waters",
      role: "moderator",
      blocked: false,
    },
    {
      id: 5,
      name: "Grace Stones",
      role: "moderator",
      blocked: true,
    },
  ],
  posts: [
    {
      id: 366283,
      title: "Introduction to Cerbos",
      content:
        "In this article, you will learn how to integrate Cerbos authorization into an existing application",
      userId: 1,
      flagged: false,
    },
  ],
};

module.exports = db;
```

 > The demo database includes five users, consisting of three members and two moderators. Among the three members, there are two active members and one blocked member. Among the two moderators, one is an active moderator and the other is a blocked moderator. 

In the meantime, the `authorization.js` will contain an empty scaffolding to see how the application works, before integrating the Cerbos authorization package:

```javascript
module.exports = async (principalId, action, resourceAtrr = {}) => {
  
};
``` 

#### Step 6

The demo application has been successfully set up. It’s now time to see how the application looks before integrating the Cerbos authorization package.

Start the server with the command below:

```
npm run start
```

You should see the following in your terminal to indicate your application is running on port 3000:

```
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
App listening on port 3000!
```

## Testing the Application Without Authorization

Now it’s time to test the application. You can use any HTTP client of your choice, such as Postman, Insomnia, or cURL. For this example, we’ll use cURL. 

Make the following requests—you should find no restrictions. Change the user_ID from 1 through 5, and you should receive a valid response. 

### Create Post

```javascript
curl --location --request POST 'http://localhost:3000/posts/' --header 'user_id: 1' --header 'Content-Type: application/json' --data-raw '{
    "title": "Introduction to Cerbos",
    "content": "Welcome to Cerbos authorization package"
}'
```

### Update Post

```javascript
curl --request PATCH 'http://localhost:3000/posts/351886' --header 'user_id: 1' --header 'Content-Type: application/json' --data-raw '{
    "title": "Welcome to Cerbos",
    "content": "10 things you need to know about Cerbos"
}'
```

### View All Posts

```javascript
curl --request GET 'http://localhost:3000/posts/' --header 'user_id: 1'
```

### View Single Post

```javascript
curl --request GET 'http://localhost:3000/posts/366283' --header 'user_id: 1'
```

### Flag Post

```
curl --request POST 'http://localhost:3000/posts/flag/366283' --header 'user_id: 5' --header 'Content-Type: application/json' --data-raw '{
    "flag": true
}'
```

### Delete Post

```javascript
curl --request DELETE 'http://localhost:3000/posts/366283' --header 'user_id: 1'
```

## Integrating Cerbos Authorization

As things stand, the application is open to authorized and unauthorized actions. Now, it’s time to implement Cerbos to ensure users perform only authorized operations.

To get started, a policy folder needs to be created to store Cerbos policies. Cerbos uses these policies to determine which users have access to what resources. In the blog post directory, run the command below to create a directory called Cerbos. This will contain the policy directory:

```
mkdir cerbos && mkdir cerbos/policies
```

Next, switch to the policies folder and create two policy YAML files: `derived_roles.yaml` and `resource_post.yaml`.

### The `derived_roles.yaml` File Description

Derived roles allow you to create dynamic roles from one or more parent roles. For example, the role *member* is permitted to view all blog posts created by other members, but is not allowed to perform any edit operation. To allow owners of a blog post who are also members make edits on their blog post, a derived role called *owner* is created to grant this permission.

Now paste the code below in your `derived_roles.yaml`:

```yaml
---
# derived_roles.yaml

apiVersion: "api.cerbos.dev/v1"
derivedRoles:
  name: common_roles
  definitions:
    - name: all_users
      parentRoles: ["member", "moderator"]
      condition:
        match:
          expr: request.principal.attr.blocked == false

    - name: owner
      parentRoles: ["member"]
      condition:
        match:
          all:
            of:
              - expr: request.resource.attr.userId == request.principal.attr.id
              - expr: request.principal.attr.blocked == false
      
    - name: member_only
      parentRoles: ["member"]
      condition:
        match:
          expr: request.principal.attr.blocked == false
    
    - name: moderator_only
      parentRoles: ["moderator"]
      condition:
        match:
          expr: request.principal.attr.blocked == false

    - name: unknown
      parentRoles: ["unknown"]
```

– _**apiVersion**_ is the current version of the Cerbos derived role.
– _**derivedRoles**_ contains the list of user roles that your application will be used for; each role will be configured based on the needs of the application.
– _**derivedRoles (name)**_ allows you to distinguish between multiple derived roles files in your application that can be used in your resource policies.
– _**derivedRoles (definitions)**_ is where you’ll define all the intended roles to be used in the application.
– _**name**_ is the name given to the derived roles generated; for example, a resource could be accessed by members and moderators. With the help of derived roles, it’s possible to create another role that will grant permissions to the resource.
– _**parentRoles**_ are the roles to which the derived roles apply, e.g. members and moderators.
– _**condition**_ is a set of expressions that must hold true for the derived roles to take effect. For example, you can create derived roles from members and moderators, then add a condition that the derived roles can only take effect if members or moderators are active. This can be done through the condition key. For more information on conditions, check the condition guide [here](https://docs.cerbos.dev/cerbos/policies/conditions.html).

### The `resource_post.yaml` File Description

The resource policy file allows you to create rules for parent/derived roles on different actions that can be performed on a resource. These rules inform the roles if they have permission to perform certain actions on a resource.

Paste the following code in your `resource_post.yaml`:

```yaml
---
# resource_post.yaml

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: "default"
  importDerivedRoles:
    - common_roles
  resource: "blogpost"
  rules:
    - actions: ['view:all']
      effect: EFFECT_ALLOW
      derivedRoles:
        - all_users

    - actions: ['view:single']
      effect: EFFECT_ALLOW
      roles:
        - moderator
        - member

    - actions: ['create']
      effect: EFFECT_ALLOW
      derivedRoles:
        - member_only

    - actions: ['update']
      effect: EFFECT_ALLOW
      derivedRoles:
        - owner
        - moderator_only
      condition:
        match:
          any:
            of:
              - expr: request.resource.attr.flagged == false && request.principal.attr.role == "member"
              - expr: request.resource.attr.flagged == true && request.principal.attr.role == "moderator"

    - actions: ['delete']
      effect: EFFECT_ALLOW
      derivedRoles:
        - owner

    - actions: ['flag']
      effect: EFFECT_ALLOW
      derivedRoles:
        - moderator_only
```

The resource policy file contains the permissions each role or derived roles can have access to:

– _**apiVersion**_ is the version for the resource policy file.
– _**resourcePolicy**_ holds all the key attributes of the resource policy.
– _**version**_ is used to identify the policy that should be used in the application; you can have multiple policy versions for the same resource.
– _**importDerivedRoles**_ is used to specify the type of derived roles you want to import into the resource policy file.
– _**resource**_ contains the resource you want to apply the roles and permissions to.
– _**rules**_ is where you will set the rules for different operations, on the basis of user permissions.
– _**actions**_ are operations to be performed.
– _**effect**_ is to indicate whether to grant the user access to the operation, based on the roles and derived roles (and conditions, if they exist).
– _**derivedRoles**_ contains the derived roles you formed in your `derived_roles yaml` file.
– _**roles**_ are static default roles used by your application.
– _**condition**_ specifies conditions that must be met before access can be granted to the operation.

> To ensure your policy’s YAML files do not contain errors, run this command in the blog post root directory. If it doesn’t return anything, then it is error-free:

```
docker run -i -t -v $(pwd)/cerbos/policies:/policies ghcr.io/cerbos/cerbos:0.10.0 compile /policies
```

## Spinning Up the Cerbos Server

You’ve now successfully created the policy files that Cerbos will be using to authorize users in your application. Next, it’s time to spin the Cerbos server up by running the below command in your terminal:

```
docker run --rm --name cerbos -d -v $(pwd)/cerbos/policies:/policies -p 3592:3592 ghcr.io/cerbos/cerbos:0.6.0
```

Your Cerbos server should be running at [http://localhost:3592](http://localhost:3592). Visit the link, and if no error is returned the server is working fine. 

## Implementing Cerbos Into the Application

Now it’s time to fill the empty scaffolding in the `authorization.js` file:

```javascript
const { Cerbos } = require("cerbos");
const db = require("./db");

const cerbos = new Cerbos({
  hostname: "http://localhost:3592", // The Cerbos PDP instance
});

module.exports = async (principalId, action, resourceAtrr = {}) => {
  const user = db.users.find((item) => item.id === Number(principalId));

  const cerbosObject = {
    actions: ["create", "view:single", "view:all", "update", "delete", "flag"],
    resource: {
      policyVersion: "default",
      kind: "blogpost",
      instances: {
        post: {
          attr: {
            ...resourceAtrr,
          },
        },
      },
    },
    principal: {
      id: principalId || "0",
      policyVersion: "default",
      roles: [user?.role || "unknown"],
      attr: user,
    },
    includeMeta: true,
  };

  const cerbosCheck = await cerbos.check(cerbosObject);

  const isAuthorized = cerbosCheck.isAuthorized("post", action);

  if (!isAuthorized)
    throw new Error("You are not authorized to visit this resource");
  return true;
};
```

The `cerbosObject` is the controller that checks if a user has access to certain actions. It contains the following keys:

– _**Actions**_ contains all of the available actions you’ve created in the resource policy file.
– _**Resource**_ allows you to indicate which resource policy you want to use for the resource request from multiple resource policy files.
	– The **policyVersion** in the resource key maps to **version** in the resource policy
               file.
	– **kind** maps to **resource** key in the resource policy file.
	– Instances can contain multiple resource requests that you want to test against the
               resource policy file. In the demo, you are only testing the blog post resource.
– _**Principal**_ contains the details of the user making the resource request at that instance.

The `cerbosCheck.isAuthorized()` method is used to check if the user/principal is authorized to perform the requested action at that instance.

## Testing Cerbos Authorization with the Blog Post Application

You have successfully set up the required roles and permissions for each operation in the CRUD blog post demo application. It’s now time to test the routes again and observe what happens, using the table below as a guide for testing:

|action  | user_id  | user_role | user_status  | response 
|---|---|---|---|---|
|create, view:all, view:single  | 1 and 2 | member | active | OK |
|All Actions|3|member|blocked|Not authorized|
|All Actions|5|moderator|blocked|Not authorized|
|Update its own post|1|member|active|OK|
|Update another user post|1|member|active|Not authorized|

The above table displays a subset of the different permissions for each user implemented in the demo application. 

You can clone the demo application repository from [GitHub](https://github.com/tolustar/cerbos-authorization). Once you’ve cloned it, follow the simple instructions in the README file. You can run the automated test script to test for the different user roles and permissions.

## Conclusion

In this article, you’ve learned the benefits of Cerbos authorization by implementing it in a demo Node.js application. You’ve also learned the different Cerbos policy files and their importance in ensuring authorization works properly.

For more information about Cerbos, you can visit the official documentation [here](https://docs.cerbos.dev/).