DEV Community

Cover image for Authorize Users Like a Pro: Libraries That Help You Implement Access Control With Node.js
ymc9 for ZenStack

Posted on • Updated on

 

Authorize Users Like a Pro: Libraries That Help You Implement Access Control With Node.js

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. Modern apps often favor OAuth-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 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

GitHub logo onury / accesscontrol

Role and Attribute based Access Control for Node.js

AccessControl.js

Build Status Coverage Status Dependencies Known Vulnerabilities Maintained
npm Release Downloads/mo. License TypeScript Documentation
© 2019, Onur Yıldırım (@onury).

Role and Attribute based Access Control for Node.js

Many RBAC (Role-Based Access Control) implementations differ, but the basics is widely adopted since it simulates real life role (job) assignments. But while data is getting more and more complex; you need to define policies on resources, subjects or even environments. This is called ABAC (Attribute-Based Access Control).

With the idea of merging the best features of the two (see this NIST paper); this library implements RBAC basics and also focuses on resource and action attributes.

Core Features

  • Chainable, friendly API. e.g. ac.can(role).create(resource)
  • Role hierarchical inheritance.
  • Define grants at once (e.g. from database result) or one by one.
  • Grant/deny permissions by attributes defined by glob notation (with nested object support).
  • Ability to filter data (model) instance by allowed attributes.
  • Ability to control…

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:

// 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);
Enter fullscreen mode Exit fullscreen mode

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):

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();
    }
});
Enter fullscreen mode Exit fullscreen mode

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

GitHub logo remult / remult

A CRUD framework for full stack TypeScript

Remult

A CRUD framework for full-stack TypeScript

CircleCI GitHub license npm version npm downloads Join Discord Twitter URL






Video thumbnail




Watch code demo on YouTube here (14 mins)


What is Remult?

Remult is a full-stack CRUD framework that uses your TypeScript entities as a single source of truth for your API, frontend type-safe API client and backend ORM.

  • Zero-boilerplate CRUD API routes with paging, sorting, and filtering for Express / Fastify / Next.js / NestJS / Koa / others...
  • 👌 Fullstack type-safety for API queries, mutations and RPC, without code generation
  • Input validation, defined once, runs both on the backend and on the frontend for best UX
  • 🔒 Fine-grained code-based API authorization
  • 😌 Incrementally adoptable
  • 🚀 Production ready

Status

Remult is production-ready and, in fact, used in production apps since 2018 However, we’re keeping the major version at zero so we can use community feedback to finalize the v1 API.

Motivation

Full-stack web…

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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

// the code works in both frontend and backend

const posts = remult.repo(Post).find();
...
await remult.repo(Post).update(id, {title: "'my title'});"
Enter fullscreen mode Exit fullscreen mode

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:

import { BackendMethod, remult } from "remult";

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

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

GitHub logo zenstackhq / zenstack

Supercharges Prisma ORM with a powerful access control layer and unlocks its full potential for web development.

What it is

ZenStack is a toolkit that simplifies the development of a web app's backend. It supercharges Prisma ORM with a powerful access control layer and unleashes its full potential for web development.

Our goal is to let you save time writing boilerplate code and focus on building real features!

How it works

ZenStack extended Prisma schema language for supporting custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma.

// schema.zmodel

model Post {
    id String @id
    title String
    published Boolean @default(false)
    author User @relation(fields: [authorId], references: [id])
    authorId String

    // 🔐 allow logged-in users to read published posts
    @@allow('read', auth() != null && published)

    // 🔐 allow full CRUD by author
    @@allow('all', author == auth())
}
Enter fullscreen mode Exit fullscreen mode

At runtime…

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) 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:

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

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])
}
Enter fullscreen mode Exit fullscreen mode

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

// sample front-end code

const { find, update } = usePost();
const {data: publishedPosts} = find({ where: { published: true }});
...
await update(postId, { title: "newTitle });"
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Interesting, thank you. One of the examples that you call "attribute based access control" I just call it business validation: The second one. I just return a 400 response on that one.

Also what about claim-based access control, like ASP.Net?

Collapse
 
ymc9 profile image
ymc9

Thank you, @webjose !

I agree with you that some checks can also be part of business validation if the app adopts an authentication-authorization-validation model. For uncomplicated cases, I feel it sufficient to unify the last two into one authorization layer, though. The usage of HTTP status is then subject to this mental model then, I guess.

I'm not too familiar with ASP.NET. To my knowledge, claims can be considered as subjects' attributes. If we take a broader definition for ABAC and let attributes cover both the subject and the resource, I think it makes sense to include claim-based access control as ABAC as well.

What do you think?

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Yes, claims are attributes. When you do JWT and store these attributes in the payload, you are making trusted claims (because of the token's digital signature). This means that you can trust the data there. Imagine you have an age claim, and you want to forbid access to a resource to users that are 18 years old or older. With claim-based authentication, you can.

Generally speaking you do policy-based authentication. A policy can be a mix of claim-based and role-based authentication. It is the most flexible of them all and the one I do all the time.

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓