Editor's Note: This is the second, technical post in a 3-part series on focusing on Authorization. Stay tuned for the last part in this series, focusing on dynamic authorization.

TL;DR: In this article, you will learn how you can leverage Auth0's RBAC (Role-Based Access Control) feature to handle end-user authorization in your APIs. The article will start by proposing a simple scenario where you could use RBAC to secure API endpoints; then, it will show how you can map this scenario in your Auth0 dashboard and how you can implement it on a Node.js and Express API. Although the samples shown here use this specific combination of technologies (Node.js and Express), you will see that the solution is easy to implement and that you can translate it to any other platform you might be using.

Important Note: This article takes advantage of the Groups feature that is currently in beta testing. To get access to this feature, please, follow the instructions on the New Beta Program for Authorization Groups announcement.

"Learn how RBAC can help you manage user permissions with ease."

What is RBAC (Role-Based Access Control)? An Introduction

Role-Based Access Control (RBAC) refers to the idea of assigning permissions to users based on their role within an organization. This approach provides fine-grained control and offers a simple and manageable approach to access management that is less prone to error than assigning permissions to users individually.

When using RBAC, you analyze the system needs of your users and group them into roles based on common responsibilities and needs. You then assign one or more roles to each user and one or more permissions to each role. The user-role and role-permissions relationships make it simple to perform user assignments since users no longer need to be managed individually, but instead have privileges that conform to the permissions assigned to their role(s).

The RBAC Scenario

Now that you know what RBAC stands for and have an abstract idea of how it works, it is time to put this authorization strategy into a more realistic context. Throughout this article, you will deal with the fictitious scenario of a company called TroubleShoo. This company, as many out there, have a task force that is divided into two types of collaborators: employees and contractors.

Employees are collaborators that are hired directly by the company and that have some benefits like paid time off (vacations) and a salary. Contractors, on the other hand, do not have paid time off and they don't have a salary, so they have to submit invoices to the company. With this scenario in mind, you will use the RBAC concept to map two roles:

  • Vacation Requester: users with this role will have access to a tool that allows them to request vacations.
  • Invoice Submitter: users with this role will have access to a tool where they can send their invoices.

Then, you will assign the first role ("Vacation Requesters") to employees of the company and the second one ("Invoice Submitter") to contractors. Besides that, to add some spice to the scenario, you will create yet another role that you will attach both to employees and to contractors:

  • Expense Submitter: users with this role will have access to a tool that allows them to submit expenses.

Graphic showing defined Roles for the RBAC expense submitted demo tutorial

Another interesting feature you will learn about in this article is the "Groups" one. Although you could use plain RBAC and assign the roles directly to users, you will use a better strategy that consists of defining groups to work as an intermediary between users and roles. That is, instead of assigning roles to users directly, you will assign roles to groups and users to groups.

When you start having dozens of roles and users, having groups on top of roles can be really helpful. For example, imagine you have 100 employees, and you need to assign a new role to them. Instead of having to update each user, if you have an Employee group, you can assign the new role to this group, and then the users will inherit it automatically.

So, back to the TroubleShoo scenario, you will have the Employees group and the Contractors group. Then you will use these groups as follow:

  • Employees: this group will have the Vacation Requester and the Expense Submitter roles
  • Contractors: this group will have the Expense Submitter and the Invoice Submitter roles

By having these groups, you can assign users directly to them, which will make user management tasks easier. This will give you a good idea of how you can leverage RBAC and Groups to assist you in authorization processes.

Graphic showing defined Groups for the RBAC expense submitted demo tutorial

Apps, APIs, and RBAC

As mentioned before, to learn how to handle RBAC rules in your apps, you will use Node.js and Express to run APIs that represent the resources your users can consume. However, instead of making you invest time to create Express APIs from scratch, the following sections will ask you to clone a starter project from GitHub. This will allow you to focus just on the code that matters for this tutorial.

Also, to make the whole exercise more realistic, instead of asking you to test the APIs and the RBAC rules through some HTTP client, you will use an app that is deployed on the cloud to consume these APIs. Using this app will make the whole article flow more easily.

In summary, this is what you will have at the end of the article:

  • A Node.js and Express application that exposes endpoints that relate to the roles presented before (i.e., endpoint where users will be able to manage vacations, invoices, and expenses).
  • Access to a SPA application deployed on the cloud to consume these endpoints.
  • An Auth0 account configured with the groups and roles introduced by the last section.

Using Auth0 to Map the RBAC Scenario

After learning about the scenario that you will work on, you can jump right into mapping it on Auth0. If you don't have a free Auth0 account yet, now is a good time to create one. After creating it (or logging into an existing one), open your Auth0 dashboard and search for the APIs section. There, you will need to create three different APIs: Vacation API, Expense API, and Invoice API.

To start mapping these APIs, click on Create API and fill the dialog shown as follows:

  • Name: "Vacation API"
  • Identifier: https://vacation-api.troubleshoo.com

Then, click on the Create button and go to the Permissions tab of your new API. There, you will have to create two permissions:

  • create:vacations: use "Create vacation requests" as the description
  • read:vacations: use "Read vacation requests" as the description

With that in place, move to the Settings tab and turn on the "Enable RBAC" and the "Add Permissions in the Access Token" options; then, hit the Save button to persist the changes. You need these options turned on so you get the information about what each user have access to in your Node.js and Express endpoints.

Note: Your APIs will get information about users through access tokens that are generated by Auth0 at the moment they authenticate in an application. If you want to learn more about this process, please, refer to the Learn Identity course we released recently.

Now, repeat the whole API creation process to create the other APIs. You can use the following data to create the other two:

  • Expense API
    • Name: "Expense API"
    • Identifier: https://expense-api.troubleshoo.com
    • create:expenses: "Create expense reports"
    • read:expenses: "Read expense reports"
  • Invoice API
    • Name: "Invoice API"
    • Identifier: https://invoice-api.troubleshoo.com
    • create:invoices: "Create invoice reports"
    • read:invoices: "Read invoice reports"

Also, make sure you turn on "Enable RBAC" and "Add Permissions in the Access Token" on both APIs.

After creating these APIs, go to the Roles section to create the three roles you will need for this exercise. To start, click on Create Role and use the following information to fill the dialog:

  • Name: "Vacation Requester"
  • Description: "Role that gives users permissions to request vacations"

Then, after clicking on Create, head to the Permissions tab and click on Add Permissions. When Auth0 presents the new dialog to you, choose Vacation API on the drop-down list, then select both of the permissions presented. After that, click on Add Permissions to complete the process.

With one Role created, there are two more to go. To create these other Roles, repeat the steps above while using the following information:

  • Expense Submitter:
    • Name: "Expense Submitter"
    • Description: "Role that gives users permissions to create expense reports"
    • API: "Expense API"
    • Permissions: create:expenses and read:expenses
  • Invoice Submitter:
    • Name: "Invoice Submitter"
    • Description: "Role that gives users permissions to submit invoices"
    • API: "Invoice API"
    • Permissions: create:invoices and read:invoices

When you complete this process, you can head to the Groups section of your dashboard to create the Employees and Contractors groups. When you get there, click on Create Group and fill the dialog as follows:

  • Name: "Employees"
  • Description: "Internal workforce"
  • Connection: "Username-Password-Authentication"

Note: If you are using a brand new Auth0 Account, you will certainly have the "Username-Password-Authentication" connection. This connection refers to the user database that Auth0 runs for you to persist data from users that sign up with username and password. If you are using an existing account, this connection might not exist anymore, and other connections might be available. In this case, you can head to the "Database Connections" and create the "Username-Password-Authentication" for this exercise.

After creating the new group, click on the Roles tab, and assign the "Vacation Requester" and "Expense Submitter" roles to it. Then, repeat the process to create the "Contractors" group:

  • Name: "Contractors"
  • Description: "External workforce"
  • Connection: "Username-Password-Authentication"
  • Roles: "Expense Submitter" and "Invoice Submitter"

When you finish creating these groups, the next thing you will have to do is to create some users to test the whole thing. To do so, head to Users, click on Create User, and create a couple of employees:

  • Employee 1:
    • Email: "employee1@troubleshoo.com"
    • Password: "employee1@troubleshoo.com"
    • Connection: "Username-Password-Authentication"
  • Employee 2:
    • Email: "employee2@troubleshoo.com"
    • Password: "employee2@troubleshoo.com"
    • Connection: "Username-Password-Authentication"

Then, create a couple of contractors:

  • Contractor 1:
    • Email: "contractor1@somewhere.com"
    • Password: "contractor1@somewhere.com"
    • Connection: "Username-Password-Authentication"
  • Contractor 2:
    • Email: "contractor2@anotherplace.com"
    • Password: "contractor2@anotherplace.com"
    • Connection: "Username-Password-Authentication"

After that, open each user and use their Groups tab to assign them to groups. In this case, assign "employee1@troubleshoo.com" and "employee2@troubleshoo.com" to the Employees group, and assign "contractor1@somewhere.com" and "contractor2@anotherplace.com" to the Contractors group.

When you finish assigning users to groups, the last thing you will have to do on your Auth0 dashboard is to create an Auth0 Application to represent the app you are going to use while testing the solution. To create this entity, head to the Applications section, click on Create Application, and fill the dialog as follows:

  • Name: TroubleShoo Dashboard
  • Application Type: Single Page Web Applications

Then, after hitting the Create button, head to the Settings tab of your new application. When you get there, update three fields:

  • Allowed Callback URLs: https://troubleshoo.now.sh/callback
  • Allowed Web Origins: https://troubleshoo.now.sh
  • Allowed Logout URLs: https://troubleshoo.now.sh

These settings will enable the application to connect to your Auth0 account.

Note: If you are interested in learning more about Auth0 Applications, check out our docs.

Now you can scroll to the bottom of the page and hit Save Changes.

Handling RBAC in Node.js and Express

Now that you have finished mapping the elements you will need in your Auth0 dashboard, it is time to play with some code. As mentioned, you won't need to build an API from scratch in this exercise. Instead, you will fetch a pre-existing project from GitHub and will focus on securing it with RBAC.

So, open a terminal, move to the directory where you usually save your projects, and issue the following commands:

# clone the project locally
git clone https://github.com/auth0-blog/rbac-nodejs.git

# move into the project
cd rbac-nodejs

# install project dependencies
npm install

After cloning and installing its dependencies, run the project (by issuing npm start), then open a new terminal and issue some HTTP requests to it to confirm that the API is working and that its endpoints are not secured:

# issue an HTTP GET request 
curl http:/localhost:3001/vacations

If everything works as expected, you will get an empty array ([]) as the result of the request, and you will see that the API will log something similar to this:

::1 - - [25/Jul/2019:17:43:12 +0000] "GET /vacations HTTP/1.1" 200 2 "-" "curl/7.54.0"

After confirming the API works properly, open the project in your preferred code editor, and create a new file called authorizationUtils.js inside the src directory. Then, insert the following code in this file:

const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

function checkAccessToken(issuer, audience) {
  return jwt({
    secret: jwksRsa.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${issuer}/.well-known/jwks.json`
    }),

    // Validate the audience and the issuer.
    audience,
    issuer: `https://${issuer}/`,
    algorithms: ['RS256']
  })
}

function checkPermission(permission) {
  return (req, res, next) => {
    const {permissions} = req.user;
    if (permissions.includes(permission)) return next();
    res.status(403).send();
  }
}

module.exports = {
  checkAccessToken,
  checkPermission,
};

The code above defines two functions:

  • checkAccessToken: This function uses the express-jwt and the jwks-rsa packages to validate access tokens. Then, if they are valid, they populate the req.user property with details about who is calling the API.
  • checkPermission: This function creates a middleware that validates if the caller (req.user) contains the permission needed.

Note: If you would like to learn more details about how to secure Node.js and Express APIs with access tokens, check the "Node.js and Express Tutorial: Building and Securing RESTful APIs" blog post.

After defining these functions, open the invoicesAPI.js (you can find it inside the apis directory) and replace its code with the following one:

const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertInvoice, getInvoices} = require('../database/invoices');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.INVOICE_API));

// endpoint to return all invoice requests
router.get('/', checkPermission('read:invoices'), async (req, res) => {
  res.send(await getInvoices());
});

// endpoint to insert new invoice request
router.post('/', checkPermission('create:invoice'), async (req, res) => {
  const newInvoice = req.body;
  await insertInvoice(newInvoice);
  res.send({ message: 'Invoice request inserted.' });
});

module.exports = router;

The new version of this module is importing and using the two functions you just defined: checkAccessToken and checkPermission. First, this module is making the router defined there use checkAccessToken to confirm that the token was issued by a trustworthy partner and that it was issued to this API specifically. Then, the file is using the checkPermission function to guarantee that callers have the read:invoices and create:invoice permissions before passing the request to the endpoints.

These small changes are everything you will need in this file in particular. Now, you need to secure the other two APIs. To do so, open the expensesAPI.js file and replace its code with this:

const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertExpense, getExpenses} = require('../database/expenses');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.EXPENSE_API));

// endpoint to return all expense reports
router.get('/', checkPermission('read:expenses'), async (req, res) => {
  res.send(await getExpenses());
});

// endpoint to insert new expense report
router.post('/', checkPermission('create:expenses'), async (req, res) => {
  const newExpense = req.body;
  await insertExpense(newExpense);
  res.send({ message: 'Expense report inserted.' });
});

module.exports = router;

Then, open the vacationsAPI.js file and replace its code with this:

const express = require('express');
const {checkAccessToken, checkPermission} = require('../authorizationUtils');

const {insertVacation, getVacations} = require('../database/vacations');

const router = express.Router();

router.use(checkAccessToken(process.env.ISSUER, process.env.VACATION_API));

// endpoint to return all vacation requests
router.get('/', checkPermission('read:vacations'), async (req, res) => {
  res.send(await getVacations());
});

// endpoint to insert new vacation request
router.post('/', checkPermission('create:vacations'), async (req, res) => {
  const newVacation = req.body;
  await insertVacation(newVacation);
  res.send({ message: 'Vacation request inserted.' });
});

module.exports = router;

The changes you are making to both files are quite similar to the ones you made in the first file. The only difference is that you are using other permissions to secure the endpoints they expose. More specifically, you are using read:expenses and create:expenses to secure the expenses endpoints, and you are using read:vacations and create:vacations to secure the vacations endpoints.

With these changes in place, create a file called .env on the project root and insert the following variables to it:

ISSUER=${SET_YOUR_ISSUER}
EXPENSE_API=${SET_YOUR_EXPENSE_API}
INVOICE_API=${SET_YOUR_INVOICE_API}
VACATION_API=${SET_YOUR_VACATION_API}

Note that, this time, before running the API again, you will need to configure four environment variables:

  • ISSUER: This is the domain of your Auth0 tenant (e.g., rbac-and-groups.auth0.com). You can find it on the Domain field of the "TroubleShoo Dashboard" application you created in your Auth0 account.
  • EXPENSE_API: The identifier you used to create the Expense API (e.g., https://expense-api.troubleshoo.com)
  • INVOICE_API: The identifier you used to create the Invoice API (e.g., https://invoice-api.troubleshoo.com)
  • VACATION_API: The identifier you used to create the Vacation API (e.g., https://vacation-api.troubleshoo.com)

After replacing the placeholders with your values, rerun the application by issuing npm start. Then, open a web browser and point it to https://troubleshoo.now.sh. This is the application you will use to test the RBAC rules you configured in your API.

The TroubleShoo dashboard for testing the RBAC rules

After opening this app, click on the Config option, and update its configuration properties to reflect your environment. There are at least two properties you will need to update:

  • Auth0 Domain: This is the same domain you used on the ISSUER environment variable, which is the domain of your Auth0 tenant (e.g., rbac-and-groups.auth0.com).
  • Auth0 Client ID: This is the Client ID of the Auth0 Application you created to represent this app.

Besides that, you might have to change other properties if you used different values to create the Auth0 APIs or if you don't have the default database connection (Username-Password-Authentication) anymore. After changing the values accordingly, hit the Save button at the bottom of this page.

Note: Even though this application is running on a real web server on the internet, the HTTP requests it will issue will go to the API your are running locally. If, for whatever reason, you changed the port of your local API, you can use the API URL field on the configuration page to define what port to use.

At this moment, you can start playing with the application and your APIs. For example, if you try to use any of the three features (Vacations, Expenses, and Invoices) without signing in first, you will end up getting an error dialog on the browser. Now, if you sign in with any of the fictitious employees you created, you will be able to use only the Vacations and the Expenses functionalities. On the other hand, if you sign in with a contractor, you won't be able to use the Vacations feature, but you will have access to the Invoices one.

Using the TroubleShoo dashboard to test the RBAC rules that secure the HTTP APIs

"I just learned how to secure HTTP APIs with RBAC rules with ease by using @auth0."

Recap

In this article, you started by taking a quick look into RBAC and how it can help you organize the permissions in your applications. Then, you learned how to use Auth0 to map the rules of your RBAC schemas and how Groups can make the process even easier. After that, you dove into how to secure HTTP APIs with the permissions you mapped, and you even tested an API by using the TroubleShoo sample application. We hope you enjoyed the exercise and that the process is as simple as it can be.

If you have any questions, suggestions, or comments, let us know in the comments box below. Have fun!

Resources

Authorization Series