Authorize Users Like a Pro: Libraries That Help You Implement Access Control With Node.js
Tags: security, javascript, node, webdev
Published: 2022-12-16 01:28:10

Implementing security measures in a web application is a less interesting but absolutely critical task. It's not as exciting as building shiny new features. You often don't get much credit from your manager for getting securities right. But poorly implemented security can be devastating to a good product, no matter how well you solve your users' problems.

In this article, let's talk about one important aspect of security: authorization. I'll introduce several libraries to help you implement a more reliable authorization layer in your Node.js app with less effort.

## Background

### Authentication vs. Authorization

Authentication and authorization are the two main pillars of a secure application. They're related but rather different, and quite often, people confuse them and use the terms interchangeably where they shouldn't. Let's make a few clarifications first:

- 🔐 Authentication

Authentication is all about verifying who you are. It's the process of converting some credentials into identities that your system understands. The credentials can be as simple as email/password or email/[OTP](https://en.wikipedia.org/wiki/One-time_password). Modern apps often favor [OAuth](https://darutk.medium.com/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb)-based authentication, which delegates the identity verification to a trusted 3rd-party so you can avoid saving sensitive credentials in your own systems.

- ✍🏻 Authorization

Authorization, on the other hand, controls "who can take what action to which asset". Assuming a user's identity is already verified by authentication, authorization can be conceptually understood as a function like:

> request(identity, action, asset) → allow | deny

Actions are often defined as CRUD - "create", "read", "update", and "delete"; but can also be freely defined as needed by an implementer.

Authorization and "access control" are essentially the same thing.

### Role-Based Access-Control

Role-Based Access-Control (RBAC) is a traditional way of modeling authorization. In short, you define roles, assign users to roles, and then control asset access permissions by roles instead of by individual users. A user role can be anything, like privilege, department, duty, etc.

Examples:

- Admin users can delete anything
- The sales department can read revenue report

### Attribute-Based Access Control

Contrary to RBAC, Attribute-Based Access Control (ABAC) makes permission grants based on the user and the attributes of the asset she's trying to access. An attribute can be anything, like the author of a blog post, the completion status of a Todo item, the current stage of a deal in CRM, etc.

Examples:

- A blog post can only be deleted by its author.
- A deal cannot be updated when its stage is CLOSED.


RBAC and ABAC are not mutually exclusive. In practice, you’ll often find the need to combine them: use RBAC for coarse-grained control at the asset type level and ABAC for more dynamic behavior.

Check out [this post](https://www.okta.com/identity-101/role-based-access-control-vs-attribute-based-access-control/) for a more thorough introduction.

## The example scenario

To facilitate illustration, throughout this article, I will use the following blogger app as an example scenario.

Blogger app authorization requirements:

- User roles: Member and Admin
- Assets: Post
- Asset attributes:
    - Post.owner: User
    - Post.published: boolean
- Actions: CRUD
- Rules:
    - Admin has full access to all posts
    - Owner has full access to the posts she owns
    - All users have "read" access to all published posts
    - All other requests are denied

With the background set up, let's dig into the libraries.

---

## accesscontrol

### Home
{% embed https://github.com/onury/accesscontrol %}

### Introduction

**accesscontrol** is a mature authorization library that has been in the market since 2016; its latest release was in 2018. It offers a clean and fluent API that allows you to build up access policies with code. The APIs are simple and, at the same time, flexible.

The library's approach is very simple. When setting up its policy, you introduce roles and resources and specify rules for CRUD actions. Actions fall into two kinds: "own" and "any". A good common understanding is that "own" controls CRUD over resources that the current user owns, while "any" controls CRUD on all resources. However, such understanding is only a convention, and a developer can decide how to interpret them. With the setup ready, you can ask questions like "can role X update his own resource Y?" or "can role X read any resource Y?" etc.

Our example scenario can be implemented like the following:

```ts
// setting up access policies

import { AccessControl } from 'accesscontrol';

const ac = new AccessControl();

ac.grant('member')
    .createOwn('post')
    .readOwn('post')
    .updateOwn('post')
    .deleteOwn('post');

ac.grant('admin')
    .createAny('post')
    .readAny('post')
    .updateAny('post')
    .deleteAny('post');

// freeze our policy
ac.lock();

// make queries

// -> false
console.log(ac.can('member').updateAny('post').granted);

// -> false
console.log(ac.can('member').updateOwn('post').granted);

// -> true
console.log(ac.can('admin').deleteAny('post').granted);
```

What can be very surprising to new users is that although the library provides concepts of "own" and "any", it doesn’t actually check them at all. It's your responsibility to confirm whether a user "owns" a resource before querying the policy engine. In other words, when you call "readOwn", the engine assumes that you've already verified the ownership. This is similar to the relationship between Authorization and Authentication; although Authorization depends on the user’s identity, it’s not responsible for verifying it.

The right way of using it in a web backend looks like this (using Express.js as an example here):

```ts
app.put('/post/:id', async (req, res) => {
    const post = await loadPost(req.params.id);
    if (!post) {
        res.status(404).send();
        return;
    }
    let permission = ac.can(req.user.role).updateAny('post');
    if (!permission.granted) {
        if (post.ownerId === req.user.id) {
            permission = ac.can(req.user.role).readOwn('post');
        }
    if (permission.granted) {
        // do post update here
    } else {
        // resource is forbidden for this user/role
        res.status(403).end();
    }
});
```

A cautious reader might have noticed that our policy definition doesn’t cover one of the requirements:

> ❌ All users have "read" access to all published posts

You're right; unfortunately, this cannot be expressed in accesscontrol. However, it can be worked around by "bending" the concept of "own". As said, what "own" means is determined by YOU, not the library. So, in our case, if a post is published, your code can treat it as "owned" by any user when the action is "read". Problem solved.

At its core, **accesscontrol** is nothing but a permission inference system. This may look overkill for simple cases like our example, but when your app evolves to have a complex multi-level role hierarchy with many types of resources, having a central place to define access policies declaratively and not needing to write inference code can be a big benefit. It improves maintainability and lowers the chances of security bugs.

### Pros

- Easy-to-use fluent API.
- Simple concepts (although a bit brain-twisting initially), good flexibility.
- Agnostic to framework and storage.
- Supports field visibility control (not shown in the demo).

### Cons

- As a developer, you must take care of more things: identifying user's role, checking resource ownership, etc.
- It would be nice if ABAC is more naturally supported (without hacking the “own” concept as we did).
- Not integrated with storage. E.g., if you need to return a list of "readable" posts to the user, you'll have to fetch all from db and then filter with **accesscontrol**.
- Not maintained anymore.

### Best-fit

If you want a simple authorization library that doesn’t interfere with the choices of your stack, **accesscontrol** can be a great fit due to its elegant model and easy-to-use API. You can maintain the key authorization specs with it and keep the freedom to add custom logic around it.

## Remult

### Home
{% embed https://github.com/remult/remult %}

### Introduction

Remult is a toolkit for implementing CRUD apps. It provides a code-first way for you to define your application entities’ schema and allows you to attach RBAC policies to the schema. A RESTful API is then generated on-the-fly exposing CRUD operations that are guarded by the policy rules. You then can mount the API to a server like Express or Next.js and build front-end features with it.

Let's see how to express our sample scenario with Remult. First, a `Post` entity can be defined as a typescript class. Note that the entity carries annotations representing access policies.

```ts
import { Allow, Entity, Fields } from 'remult';
import { Roles } from './Roles';

@Entity<Post>('post', {
    allowApiRead: Allow.authenticated,
    allowApiInsert: Allow.authenticated,

    // a post can be updated by admin or its owner
    allowApiUpdate: (remult, post) =>
        remult.authenticated() &&
        (remult.user!.roles!.includes(Roles.admin) ||
            post!.ownerId === remult.user!.id),

    // a post can be deleted by admin or its owner
    allowApiDelete: (remult, post) =>
        remult.authenticated() &&
        (remult.user!.roles!.includes(Roles.admin) ||
            post!.ownerId === remult.user!.id),
})
export class Post {
    @Fields.uuid()
    id!: string;

    @Fields.string()
    title = '';

    @Fields.boolean()
    published = false;

    @Fields.string()
    ownerId!: string;
}
```

Then you can use the repository API (in both front-end and backend code) to access the entity.

```ts
// the code works in both frontend and backend

const posts = remult.repo(Post).find();
...
await remult.repo(Post).update(id, {title: 'my title'});
```

Under the hood, the repository API calls into the generated backend services, which are protected by the authorization policies.

You may have noticed that I haven't implemented the following requirement yet:

> ❌ All users have "read" access to all published posts

Unfortunately, we ran into a limitation of Remult that there isn't a way to access an asset's attribute (`ownerId` of `Post` here) in "read" policy. To mitigate this problem, you’ll have to implement some backend methods to support the aforementioned requirements, like:

```ts
import { BackendMethod, remult } from "remult";

export class PostsController {
   @BackendMethod({ Allow.authenticated })
   static async find() {
      // implement your custom authorization here
   }
}
```

For non-admin users, you call into this backend API instead. This works, but unfortunately, it counteracted quite a lot of the benefit of attaching access policies to the entities since information is now scattered in multiple places.

### Pros

- Access policies are collocated with data model.
- CRUD services with authorization are automatically generated.
- UI framework agnostic.
- Actively developed.

### Cons

- Remult itself is an ORM as well, so if you already decided to use another ORM (like TypeORM or Prisma), there's a conflict.
- Expressiveness of access policy is limited.

### Best-fit

Remult could be a good fit if your app has a fairly simple authorization strategy and you want to build it up fast. It generates both backend services and frontend libraries for you. If you're a fan of code-first ORM tools (e.g. [TypeORM](https://typeorm.io/)) then you'll find it intuitive to pick up as well.

However, a more sophisticated app can easily outgrow the comfort-zone of Remult, and you'll likely end up writing quite a lot of backend methods, which is not too much difference compared to implementing a formal backend.

## ZenStack

### Home
{% embed https://github.com/zenstackhq/zenstack %}

### Introduction

*DISCLAIMER: I’m the creator of ZenStack.*

ZenStack is a toolkit for simplifying building secure CRUD apps with Next.js. It shares some similarities with Remult: generating protected backend services and frontend library based on a declarative access policy model. But they also differ in several important ways:

- ZenStack is schema-first.
- It’s based on an existing ORM ([Prisma](https://prisma.io)) instead of trying to replace one.
- Its access control policy engine is both intuitive and powerful.

To use ZenStack for authorization, you add extra access policy declarations to your existing Prisma schema. Let’s see how it looks with our example scenario:

```ts
model Post {
    id String @id @default(cuid())
    title String
    published Boolean @default(false)
    owner User? @relation(fields: [authorId], references: [id])
    ownerId String?

    // must signin to access any post
    @@deny('all', auth() == null)

    // allow full CRUD by owner or admin
    @@allow('all', owner == auth() || auth().role == 'Admin')

    // published posts are readable to everyone (logged in)
    @@allow('read', published == true)
}
```

Now we've got all four authorization rules covered without any hack.

At its core, ZenStack implemented a superset of Prisma schema, and introduced two new annotations: `@@allow` and `@@deny` for expressing access policies. The policy rules can access current user and current entity, and use both information to make a verdict. It's actually capable of using relation fields and collection fields to express sophisticated rules like:

> ✅ A post is updatable by a user who is a member of editors of the post.

```ts
model Post {
	...
	// a relation field storing editors of this post
	editors User[]

	...
	// use a Collection Predicate to check if the current user
	// matches any entity in editors field
	@@allow('update', editors?[id == auth().id])
}
```

From the schema, secure RESTful services are generated for CRUD operations, together with client React hooks for consuming them.

```ts
// sample front-end code

const { find, update } = usePost();
const {data: publishedPosts} = find({ where: { published: true }});
...
await update(postId, { title: newTitle });
```

In case when the policy language is not sufficient for expressing your rules, you always have the freedom to implement a custom Next.js API endpoint, enhance the policy behavior, or completely bypass it and access database directly.

### Pros

- Access policies are collocated with data model.
- Built as an extension to an excellent ORM (Prisma).
- Seamlessly combines RBAC and ABAC.
- Authorization is deeply integrated with storage, so policy checking is fully pushed down to database for optimal performance and scalability.
- Great flexibility for defining access policies.
- Actively developed.

### Cons

- Limited to Next.js for now.
- Must use Prisma ORM.

### Best-fit

ZenStack can be an excellent fit for you if you're considering to use Prisma as your ORM, since its language syntax is mostly compatible with Prisma's schema language. It's also a good match if you anticipate to have a non-trivial authorization strategy, while still want to collocate the policy rules with data to keep a single source of truth.

---

## Wrap up

Building a secure web app is full of challenges. Implementing proper authorization is a big part of that endeavor. I hope you enjoy the reading and the tools can be of help to you in the future. If you find some other cool tools not included here, please leave a message, and I'll cover them in a future post.

Have fun!