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.
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.
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 policiesimport{AccessControl}from'accesscontrol';constac=newAccessControl();ac.grant('member').createOwn('post').readOwn('post').updateOwn('post').deleteOwn('post');ac.grant('admin').createAny('post').readAny('post').updateAny('post').deleteAny('post');// freeze our policyac.lock();// make queries// -> falseconsole.log(ac.can('member').updateAny('post').granted);// -> falseconsole.log(ac.can('member').updateOwn('post').granted);// -> trueconsole.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):
app.put('/post/:id',async (req,res)=>{constpost=awaitloadPost(req.params.id);if (!post){res.status(404).send();return;}letpermission=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/roleres.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 uses TypeScript entities as a single source of truth for: ✅ CRUD + Realtime API, ✅ frontend type-safe API client, and
✅ backend ORM.
⚡ Zero-boilerplate CRUD + Realtime API with paging, sorting, and filtering
👌 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
Remult supports all major databases, including: PostgreSQL, MySQL, SQLite, MongoDB, MSSQL and Oracle.
Remult is frontend and backend framework agnostic and comes with adapters for Express, Fastify, Next.js, Nuxt, SvelteKit, SolidStart, Nest, Koa, Hapi and Hono.
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 ownerallowApiUpdate:(remult,post)=>remult.authenticated()&&(remult.user!.roles!.includes(Roles.admin)||post!.ownerId===remult.user!.id),// a post can be deleted by admin or its ownerallowApiDelete:(remult,post)=>remult.authenticated()&&(remult.user!.roles!.includes(Roles.admin)||post!.ownerId===remult.user!.id),})exportclassPost{@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.
// the code works in both frontend and backendconstposts=remult.repo(Post).find();...awaitremult.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:
import{BackendMethod,remult}from"remult";exportclassPostsController{@BackendMethod({Allow.authenticated})staticasyncfind(){// 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) 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.
Fullstack TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
ZenStack is a Node.js/TypeScript toolkit that simplifies the development of a web app's backend. It enhances Prisma ORM with a flexible Authorization layer and auto-generated, type-safe APIs/hooks, unlocking its full potential for full-stack development.
Our goal is to let you save time writing boilerplate code and focus on building real features!
How it works
Read full documentation at 👉🏻 zenstack.dev. Join Discord for feedback and questions.
ZenStack incrementally extends Prisma's power with the following four layers:
1. ZModel - an extended Prisma schema language
ZenStack introduces a data modeling language called "ZModel" - a superset of Prisma schema language. It extended Prisma schema with custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma.
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:
modelPost{idString@id@default(cuid())titleStringpublishedBoolean@default(false)ownerUser?@relation(fields:[authorId],references:[id])ownerIdString?// 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.
modelPost{...// a relation field storing editors of this posteditorsUser[]...// 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.
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.