---
title: "Implement authorization and access control in an Express application"
description: "Explore importance of roles and permissions in webapps and learn how to use Cerbos authorization to enforce RBAC for a sample LMS implemented in Express and Node."
author: "Adejoke Haastrup"
date: "2026-04-14T20:59:00.000Z"
canonical: "https://www.cerbos.dev/blog/implement-authorization-in-express"
image: "https://stylish-appliance-1c1cc1c30d.media.strapiapp.com/implement_authz_access_control_express_dbff81fcd7.png"
tags: ["guide"]
source: "https://www.cerbos.dev/blog/implement-authorization-in-express"
---

# Implement authorization and access control in an Express application

# Authorization and access control in Express

When building web apps, one of the important factors you should consider is how users will access various parts of your app. It is best practice to manage user permissions and access by restricting or granting access to certain application areas. In this post, we'll look at how to use Cerbos PDP (Policy Decision Point) to establish role-based access control ([RBAC](https://www.cerbos.dev/features-benefits-and-use-cases/rbac)) in an Express application.

## Getting started with Express and understanding Cerbos PDP (Policy Decision Point)

If you're familiar with Node.js, you've probably heard of Express. Many engineers opt to use Express, a Node.js framework, for developing JavaScript web apps. Cerbos PDP can be described as an engine for authorizing access to different parts of your app based on certain rules and policies you set.

## Introduction to Role-Based Access Control (RBAC)

RBAC is all about assigning permissions to specific roles rather than individual users. Once roles are defined, users are assigned to these roles based on their responsibilities.

## Understanding roles, permissions, and users

Let's say for example, you are building a learning management system. This will call for properly defined roles in your web app.

- Roles: Representing a users function within a system. In our LMS example, to define a role you need to identify all the expected roles in your app E.g super admin, admin, users.
- Permissions: Now that you have well defined roles, you have to grant specific privileges to the roles. For example only a super admin can upload and delete videos.
- Users: Individuals on the app who are assigned predefined roles based on their specific responsibilities.

# How-To guide: Practical RBAC implementation in Express with Cerbos PDP

Following our LMS example, we are going to build a sample project to demonstrate the implementation of RBAC in Express with Cerbos PDP.

## Development environment setup

Before we begin, there are a few prerequisites you need to have in place. Please ensure that Node.js, Express, and most importantly, Cerbos PDP are installed on your system.

### Node.js and Express

Install [Node.js](https://nodejs.org/en/download/) for your platform (we recommend LTS) and verify the installation by running the following command in your terminal:

```shell
node -v
```

Once Node.js is installed, install Express as well:

```shell
npm install -g express
```

### Cerbos

Install the [Cerbos PDP](https://docs.cerbos.dev/cerbos/latest/installation/binary) for your platform, and ensure that the binary is in your `$PATH` (or run it from the extracted location):

Once installed, test it to make sure that it can execute:

```shell
cerbos --version
```

## Sample LMS to demonstrate RBAC

Once you're confident that Node.js, Express, and Cerbos PDP are installed, you can start a new project running these commands

```shell
mkdir lms-cerbos
cd lms-cerbos
```

This will create a new folder called `lms-cerbos` and navigate into it.

Next, initialize a new Node.js project:

```shell
npm init -y
```

### Install all required packages

Install the necessary packages, including TypeScript and Axios:

```shell
npm install axios dotenv
npm install --save-dev typescript ts-node @types/node @types/express
```

Initialize a TypeScript configuration file:

```shell
npx tsc --init
```

### Cerbos PDP setup

Configure the Cerbos PDP by creating a basic policy file that defines roles and permissions. Create a `cerbos` directory and add a `policies` sub-directory.

Create a policy file `resource.courses.yaml` in the `policies/` directory:

```yaml
# policies/policy.yaml
resource: "course"
version: "default"
roles:
  - id: "super-admin"
    grants:
      - actions: ["create", "upload", "delete"]
        condition:
          match:
            resource: "course"
  - id: "admin"
    grants:
      - actions: ["update", "edit"]
        condition:
          match:
            resource: "course"
  - id: "student"
    grants:
      - actions: ["view"]
        condition:
          match:
            resource: "course"
```

### Roles and permissions

Create a `roles.ts` file to define the roles and their permissions:

```typescript
// src/roles.ts
export const roles = {
  "super-admin": ["create", "upload", "delete"],
  "admin": ["update", "edit"],
  "student": ["view"],
};
```

### Routing

Now we can set up routing in Express, where we enforce RBAC using Cerbos.

```typescript
// src/index.ts
import express, { Request, Response } from "express";
import axios from "axios";
import dotenv from "dotenv";
import { roles } from "./roles";

dotenv.config();

const app = express();
app.use(express.json());

const cerbosURL = process.env.CERBOS_URL || "http://localhost:3592/api/check";

interface User {
  id: string;
  roles: string[];
}

app.post("/course_action", async (req: Request, res: Response) => {
  const { user, action } = req.body as { user: User; action: string };

  const cerbosRequest = {
    requestId: "1",
    actions: [action],
    resource: {
      kind: "course",
      attributes: {
        owner: user.id,
      },
    },
    principal: {
      id: user.id,
      roles: user.roles,
    },
  };

  try {
    const response = await axios.post(cerbosURL, cerbosRequest);
    const isAllowed = response.data.result[0].actions[action]?.isAllowed;

    if (isAllowed) {
      res.status(200).send("Action permitted");
    } else {
      res.status(403).send("Action forbidden");
    }
  } catch (error) {
    console.error("Error communicating with Cerbos PDP:", error);
    res.status(500).send("Internal server error");
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
```

### Testing out our sample app

Compile and run the application:

```shell
npx tsc
node dist/index.js
```

💡 You can test the application on Postman by sending POST requests to the `/course_action` endpoint with different roles and actions.

Sample request:

```json
{ 
  "user": {
    "id": "user1",
    "roles": ["admin"]
  },
  "action": "edit"
}
```

<center>
<div class="hs-cta-embed hs-cta-simple-placeholder hs-cta-embed-168996984756"
  style="max-width:100%; max-height:100%; width:538px;height:276.53125px" data-hubspot-wrapper-cta-id="168996984756">
  [![BlogCTA - Hub](https://no-cache.hubspot.com/cta/default/20289770/interactive-168996984756.png)](https://cta-service-cms2.hubspot.com/web-interactives/public/v1/track/redirect?encryptedPayload=AVxigLJcZiZ24z6ZSah4IpOn8dABnds9mrKKh6e4PJqvKxet7ffhWKXW0MdjhgVQy4mpPiz%2BmP6afTEqsC16yImlV%2FKtxCF9SLhJub2UaJVdW9FJFbjqw2pDljc4t3o7wysyDZUCo3QHGt4JiG3yBqSqDFxK%2FhbxRSgua197Oqt7wTaa07SsSxBmDYqFpmLr&webInteractiveContentId=168996984756&portalId=20289770)
</div>
</center>

### Expected Responses:

- 200 OK if the action permitted.
- 403 Forbidden if the action is forbidden.

# Benefits of using Cerbos for Express authorization

Once the integration is in, the reasons to keep authorization out of your route code become clear.

The rules are decoupled from the app. All the logic that used to be spread across handlers lives in one set of policy files, so the full picture of who can do what is readable in one place instead of reconstructed from a grep. Those policies are human-readable YAML with CEL conditions, which means product and security can review and even change them without reading your Express code.

Changing a rule does not require a redeploy. The PDP hot-loads policy files, so updating a YAML file, or pushing to Git with [Cerbos Hub](https://hub.cerbos.cloud/), takes effect without restarting your Node process. That alone removes a whole class of "we need a release to flip a permission" tickets.

RBAC and ABAC live together. A single policy can match on static roles and evaluate contextual attributes in the same rule, so you can start with roles and add ownership, department, or time conditions as requirements grow, without swapping approaches.

You get an [audit log](https://www.cerbos.dev/features-benefits-and-use-cases/audit-logs) for free. Every decision can be recorded with the full principal, resource, and outcome, backed by a local file, Kafka, or Hub, and each response carries a call id you can correlate with your own logs. Building that yourself is real work you now skip.

List filtering is handled by query plans rather than per-row checks, so data-heavy endpoints stay fast. The PDP is language-agnostic, exposing REST and gRPC, so the same policies serve your Express app and any other service in your stack. It is open source and Apache-2.0, free to self-host with a docker run, and it is stateless, so you scale it like any other stateless service, as a sidecar per service or a shared deployment behind a load balancer.

# Next steps

From this article, you've learned the importance of roles and permissions. You can also see in our example how we used Cerbos to enforce RBAC for an LMS. The super-admin, admin, and student roles are defined in the Cerbos policy file with their respective permissions for the course resource. The Express app communicates with Cerbos to check if a user is authorized to perform a specific action on a course, ensuring that only the appropriate roles can create, upload, delete, update, edit, or view courses.

You now have the whole flow, run the PDP, write a policy, make one call from your route, and layer attributes and list filtering on top as you need them. 

[Try Cerbos Hub](https://hub.cerbos.cloud/) to manage and distribute these policies across environments, or [book a call](https://www.cerbos.dev/workshop) to talk through your Express setup with our team.

Go deeper:

* [Why external authorization](https://www.cerbos.dev/blog/why-external-authorization) for the case behind decoupling authorization from your app  
* [How to adopt externalized authorization](https://www.cerbos.dev/ebooks-webinars) (eBook) for a step-by-step playbook

## FAQ

### How do you implement authorization in an Express application?

You implement authorization in an Express application by deciding, on each request, whether the authenticated user is allowed to perform the requested action on the resource. The maintainable approach is to move the rules out of your route handlers and into policies evaluated by a dedicated service, so your Express route makes a single check call and returns a 403 when the decision is deny. With Cerbos, that call is checkResource, and the rules live in versioned YAML policy files rather than scattered if statements.

### What is the difference between authentication and authorization in Express?

The difference between authentication and authorization in Express is that authentication confirms who the user is, while authorization decides what that user is allowed to do. Authentication is usually handled by an identity provider or a library like Passport, producing a req.user. Authorization runs after that, checking whether req.user can perform a given action on a given resource, which is the part Cerbos handles through policies.

### How do you add role-based access control (RBAC) to an Express app?

You add role-based access control to an Express app by tagging each authenticated user with a role and gating routes on that role. Rather than hardcoding role checks in every handler, you define the roles and their allowed actions in a policy, and your route asks the authorization service whether the user's role permits the action. This keeps the rules in one place and lets you add roles or change permissions without editing route code.

### Can you use attribute-based rules and not just roles in Express authorization?

Yes, you can use attribute-based rules in Express authorization to handle cases that roles cannot express, such as letting users act only on records they own or only during business hours. With Cerbos you add a condition to a policy rule written as a CEL expression that compares attributes of the principal and the resource, and you pass those attributes in the check call. The route code stays the same, so you move from RBAC to ABAC without rewriting your middleware.

### How do you filter database results by authorization in Express?

You filter database results by authorization in Express using a query plan rather than checking each row after fetching it. Cerbos returns a filter from planResources for a given action, which you convert into a WHERE clause for your ORM using an adapter for Prisma, Drizzle, or Mongoose. The list query then returns only the records the policy allows, which keeps data-heavy endpoints fast and avoids loading rows the user cannot see.

### Why use Cerbos instead of building authorization middleware in Express?

You would use Cerbos instead of building your own Express authorization middleware to avoid the maintenance cost that shows up as an app grows. Custom middleware is quick to start but tends to sprawl across services, and every rule change becomes a code change and a deploy. Cerbos keeps the rules in decoupled YAML policies that hot-load without a redeploy, supports both RBAC and ABAC, and gives you audit logging and list filtering out of the box, so authorization stays maintainable as requirements change.
