Role based access control helper for node apps
Features:
- Easy to use
- Define roles, permissions, and claims in a declarative way
- Inherit permissions across roles
- Map roles and permission sets to user claims
- Authorize roles and permission sets against user claims
- Includes helper middlewares for express apps
- Supports RBAC and ABAC type flows
- Wildcard support for permission definitions
I wanted a better way to handle authorization in node apps. There are plenty of tools to help with authentication from different identity providers such as passport, oauth, saml, oidc, etc. But after you authenticate how do you handle authorization of specific resources and routes based on those identity claims? I've seen a lot of applications do some sort of manual checking of session state and user claims on a per-route basis and in non-uniform ways. That pattern tends to be pretty error-prone and not very scalable or maintainable as the application grows. It can also be difficult to audit, change, or verify over time.
This package lets you declaratively define your permission sets with a very simple syntax. It supports heirarchical rules via permission inheritance and allows you to map those permissions to identity claims. It includes helper functions for express apps for authorizing against permissions and roles. It also supports wildcard matching for permission definitions.
Add rbac as a dependency for your app and install via npm
npm install @danmasta/rbac --save
Require the package in your app
const RBAC = require('@danmasta/rbac');
let rbac = RBAC(roles);
Add authorization to a route
app.use('/admin', rbac.isRole('admin'), router);
name | type | description |
---|---|---|
claims |
string |
What property on the req object to find claims. Default is 'user' |
roles |
array |
Role definitions to use for authorizing against. Default is undefined |
authorizeAgainst |
string|array |
Which claims to use to authorize permissions against. If not set it will attempt to authorize against all claims. Default is undefined |
strict |
boolean |
Whether or not to allow multiple role mappings per claim. If true it will throw an error if there is more than one role defined for a claim set. Default is false |
Name | Description |
---|---|
authorizeByPermissions(claims, permissions, authorizeAgainst?) |
Verify permissions against a set of claims. Accepts a set of claims, list of permissions, and optional claim key filter to authorize against. If more than one permission is provided it will succeed only if all permissions are verified. Returns a promise that resolves with claims or rejects with an AuthorizationError |
authorizeByRoles(claims, roles, authorizeAgainst?) |
Verify roles against a set of claims. Accepts a set of claims, list of roles, and optional claim key filter to authorize against. If more than one role is provided it will succeed if any role is verified. Returns a promise that resolves with claims or rejects with an AuthorizationError |
isAuthorized(permissions, opts?) |
Helper middleware for verifying permissions on routes. Accepts an optional options object of { permissions, authorizeAgainst, redirect } . Returns a middleware function |
isRole(roles, opts?) |
Helper middleware for verifying roles on routes. Accepts an optional options object of { roles, authorizeAgainst, redirect } . Returns a middleware function |
isAuthenticated(opts?) |
Helper middleware for checking authentication state. Expects a req.isAuthenticated() function to be set. Accepts an optional options object of { redirect } . Returns a middleware function |
express() |
Helper middleware for setting functions on the req object and res.locals object for routes and view templates. Returns a middleware function |
Note: The redirect
option for middleware functions is used to set req.session.redirect
on failure, if desired
Each role object has the following signature:
name | type | description |
---|---|---|
id |
string |
ID for the role. Required or an error is thrown. Used to look up other roles by ID for permission inheritance. Default is undefined |
description |
string |
Description for the role. Default is undefined |
permissions |
string|array |
Permissions for the role. Should be an array of strings: ['posts.edit', 'posts.view'] . Default is undefined |
inherit |
string|array |
Which role ids to inherit permissions from. Default is undefined |
claims |
object |
Key/value pairs of claim names and values: { groups: ['editor', 'viewer'] } . Default is undefined |
Name | Description |
---|---|
generatePermissionTree() |
Generate the permission tree for the role. Recursively walks and merges inherited permissions. Returns a permissions object |
isAuthorized(permission) |
Verifies whether or not the role has a permission: role.isAuthorized('posts.edit') . Returns a boolean |
When describing permissions you can use wildcards *
to authorize patterns of resources. The syntax looks like:
*
At the end of a permission matches anything after and including. If you want to set a superadmin type role you can just use*
as the only permission**
Explicitly matches all from this point, alias for using*
at the end*.
Match anything one level deep
String | Description |
---|---|
* |
Matches anything at any depth |
api.* |
Matches any permission at any depth that begins with api |
api.*. |
Matches any permission at 1 depth that begins with api . Matches api.status , but not api.status.db |
api.*.view |
Matches any permission at 1 depth that begins with api and ends with view . Matches api.users.view , but not api.users or api.users.edit |
Define roles, find claims on the req.user
object, and authorize against the groups
claim
const RBAC = require('@danmasta/rbac');
const roles = [
{
id: 'admin',
inherit: undefined,
permissions: [
'*'
],
claims: {
groups: [
'admin'
]
}
},
{
id: 'editor',
inherit: [
'viewer',
],
permissions: [
'api.*.edit',
'posts.edit'
],
claims: {
groups: [
'editor'
]
}
},
{
id: 'viewer',
inherit: undefined,
permissions: [
'api.*.view',
'api.*.list',
'posts.view'
],
claims: {
groups: [
'viewer'
]
}
}
];
const rbac = RBAC(roles, { claims: 'user', authorizeAgainst: 'groups' });
Secure resources and routes with middleware by permissions
let app = express();
app.get('/post/:id', rbac.isAuthorized('posts.view'), (req, res, next) => {
res.render('post/index', req.params.id);
});
app.get('/post/:id/edit', rbac.isAuthorized('posts.edit'), (req, res, next) => {
res.render('post/edit', req.params.id);
});
app.post('/api/post/:id/edit', rbac.isAuthorized('api.post.edit'), (req, res, next) => {
res.json({ message: 'post udpated', id: req.params.id });
});
Secure all admin routes by role
let app = express();
app.use('/admin', rbac.isRole('admin'), require('./routes/admin'));
Add express helpers to req
and res.locals
objects
let app = express();
app.use(rbac.express());
Verify permissions inside a route handler function
let app = express();
app.use(rbac.express());
app.get('/post/:id', (req, res, next) => {
req.isAuthorized('posts.view').then(() => {
res.render('post/index', req.params.id);
}).catch(err => {
next(err);
});
});
Tests are currently run using mocha and chai. To execute tests run npm run test
. To generate unit test coverage reports run npm run coverage
If you have any questions feel free to get in touch