Starting from this chapter?

Clone the application repo and check out the securing-api-with-auth0 branch:

git clone git@github.com:auth0-blog/wab-ts-express-api.git \
express-ts-api \
--branch securing-api-with-auth0

Make the project folder your current directory:

cd express-ts-api

Then, install the project dependencies:

npm i

If you don't have set up any Auth0 applications, follow the steps from these previous sections:

Setting Up an Auth0 API

Connecting a Client Application With Auth0

Create a .env hidden file:

touch .env

Populate .env with this:

PORT=7000
AUTH0_ISSUER="Your Auth0 issuer value"
AUTH0_AUDIENCE="Your Auth0 audience value"

Finally, in one terminal tab, run the webpack bundler:

npm run webpack

In another terminal tab, run the server:

npm start

At this point, you are only protecting your API write endpoints against non-authenticated users, which means that anyone with an access token could make a direct request to the server to modify items in the store. That scenario is far from ideal as you don't want regular customers to change the price of an item, for example. As such, you need a mechanism to limit access to your API resources and demonstrate that being authenticated is not the same as being authorized.

A straightforward way to implement access control is to create a set of write permissions and bundle them in a menu-admin role, which you assign only to select users. Thus, only select users are authorized to modify resources in your API. Consequently, your server must enforce role verification on each API write endpoint to prevent unauthorized access.

The practice described above is known as role-based access control (RBAC), which you can implement quickly for your API using the Auth0 Dashboard. You can implement RBAC and enforce it on your server as follows:

On the Auth0 side

  • Create permissions for the Menu API you created earlier.

  • Create a role called menu-admin.

  • Assign permissions from the Menu API to the menu-admin role.

  • Create a new user and assign to it the menu-admin role.

  • Add the menu-admin role permissions to the access token created for users with the role.

On the server side

  • Define the menu-admin role permissions in a TypeScript enum.

  • Define the permissions required to access an endpoint by passing permission values as arguments to an authorization middleware function, which Express calls before the endpoint route handler.

  • Implement the authorization middleware function to determine authorization by comparing the permissions required by the endpoint against the permissions present in a user's access token.

As you can see, implementing authorization is a complex process that involves many steps. Any errors or omissions in the process of implementation can leave an API at risk of being compromised. Thankfully, you can delegate the creation and maintenance of permissions, roles, and users to Auth0 and focus only on enforcing authorization on your server.

With the plan clearly outlined, let's get started.

Create API Permissions

Open the APIs page from the Auth0 Dashboard and select the Menu API that you created earlier.

In the Menu API page, click on the Permissions tab and create three permissions by filling each row as follows (the + Add button adds a new row):

  • create:items: Create menu items

  • update:items: Update menu items

  • delete:items: Delete menu items

Next, you need to configure Auth0 to enforce role-based access control (RBAC) authorization for the Menu API. Click on the Settings tab and scroll down until you see the RBAC Settings section. Use the toggle button next to Enable RBAC to turn it on, which enforces Auth0 to evaluate RBAC authorization policies during the login transaction of a user.

Next, enable Add Permissions in the Access Token to add a permissions property to the access token created by Auth0 when a user logs in. The permissions property is a key-value pair known as a token claim. The presence of this claim is critical for the implementation of RBAC in your Express app.

Once you enable these options, make sure to press Save.

Create Access Roles

Open the Roles page from the Auth0 Dashboard and click on the Create Role button. Fill out the pop-up form as follows:

  • Name: menu-admin

  • Description: Create, update, and delete menu items.

Once done, click the Create button to complete the creation of the role.

Now, you need to associate the permissions you've created with this role, mapping it to resources of your API. Click on the Permissions tab of the role page. Once there, click on the Add Permissions button.

In the dialog that comes up, choose the Menu API from the dropdown box and select all the boxes in the "Scopes" section. Once that's done, click on the Add permissions button, which takes you back to the menu-admin role page, which now lists all its associated permissions.

A scope is a term used by the OAuth 2.0 protocol to define limitations on the amount of access that you can grant to an access token. In essence, permissions define the scope of an access token.

Get User Roles

Auth0 attaches the menu-admin role permissions as a claim to the access token, but not the role itself. The demo client application needs this information as it renders its UI conditionally based on the user role. To include the user role as a claim in the tokens that Auth0 sends to the client, you can use Auth0 Rules.

When a user logs in successfully to your application, the Auth0 authorization server sends two tokens to the client:

Access token

After a user successfully authenticates and authorizes access, the client application receives an access token from the Auth0 authentication server. The client passes the access token as a credential whenever it calls a protected endpoint of the target API. This token informs the server that the client is authorized to access the API. Through its permissions claim, the access token tells the server which actions the client can perform on which resources.

ID token

The ID Token is a JSON Web Token (JWT) that contains claims representing user profile attributes like name or email, which are values that clients typically use to customize the UI.

Using Auth0 Rules, you can add to each of these tokens a new claim, which represents the roles assigned to a user.

What are Auth0 Rules?

Auth0 Rules are JavaScript functions that execute when a user logs in to your application. They run once the authentication process is complete, and you can use them to customize and extend Auth0's capabilities. For security, your Rules code executes in a sandbox, isolated from the code of other Auth0 tenants.

You can create Auth0 Rules easily using the Auth0 Dashboard. Follow these steps to create a rule that adds user roles to tokens:

  • Open the Rules page from the Auth0 Dashboard.

  • Click on the Create Rule button.

  • Click on the Empty Rule option.

  • Provide a Name to your rule, such as Add user roles to tokens.

  • Next, replace the content of the Script section with the following function:

function(user, context, callback) {
  const namespace = 'https://menu-api.demo.com';

  if (context.authorization && context.authorization.roles) {
    const assignedRoles = context.authorization.roles;

    if (context.idToken) {
      const idTokenClaims = context.idToken;
      idTokenClaims[`${namespace}/roles`] = assignedRoles;
      context.idToken = idTokenClaims;
    }

    if (context.accessToken) {
      const accessTokenClaims = context.accessToken;
      accessTokenClaims[`${namespace}/roles`] = assignedRoles;
      context.accessToken = accessTokenClaims;
    }
  }

  callback(null, user, context);
}
  • Click the Save Changes button.

What's this rule doing?

When the user successfully authenticates, this rule function executes, receiving three parameters:

  • user: an object returned by the identity provider (such as Auth0 or Google) that represents the logged-in user.

  • context: an object that stores contextual information about the current authentication transaction, such as the user's IP address or location.

  • callback: a function to send modified tokens or an error back to Auth0. You must call this function to prevent script timeouts.

function(user, context, callback) {
  // ...
}

To keep your custom claims from colliding with any reserved or external claims, you must give them a globally unique name using a namespaced format. By default, Auth0 always enforces namespacing and silently excludes from the tokens any custom claims with non-namespaced identifiers.

Namespaces are arbitrary identifiers, so technically, you can call your namespace anything you want. For convenience, the namespace value is the API audience value set in the WAB Dashboard Demo Settings.

function(user, context, callback) {
  const namespace = 'https://menu-api.demo.com';

  //...
}

You then check if the context object has an authorization property and, in turn, if that property has a roles property:

function(user, context, callback) {
  const namespace = 'https://menu-api.demo.com';

  if (context.authorization && context.authorization.roles) {
   // ...
  }

  // ...
}

context.authorization is an object containing information related to the authorization transaction, such as roles.

context.authorization.roles is an array of strings containing the names of the roles assigned to a user.

Next, you assign the roles array to the assignedRoles constant and check if there is an ID token or access token present in the context object:

function(user, context, callback) {
  const namespace = 'https://menu-api.demo.com';

  if (context.authorization && context.authorization.roles) {
    const assignedRoles = context.authorization.roles;

    if (context.idToken) {
      // ...
    }

    if (context.accessToken) {
      // ...
    }
  }

  // ...
}

If any of these tokens are present, you add to the token object a <namespace>/roles property with the roles array, assignedRoles, as its value, effectively creating a custom claim on the token that represents the user roles:

function(user, context, callback) {
  const namespace = 'https://menu-api.demo.com';

  if (context.authorization && context.authorization.roles) {
    const assignedRoles = context.authorization.roles;

    if (context.idToken) {
      const idTokenClaims = context.idToken;
      idTokenClaims[`${namespace}/roles`] = assignedRoles;
      context.idToken = idTokenClaims;
    }

    if (context.accessToken) {
      const accessTokenClaims = context.accessToken;
      accessTokenClaims[`${namespace}/roles`] = assignedRoles;
      context.accessToken = accessTokenClaims;
    }
  }

  // ...
}

Finally, you invoke the callback function to send the potentially modified tokens back to Auth0, which in turn sends them to the client:

function(user, context, callback) {
  // ...

  callback(null, user, context);
}

That's all you need to create an Auth0 rule that adds user roles to tokens. What's left to do is for you to create a user that has the menu-admin role.

Create an Admin User

Open the Users page from the Auth0 Dashboard and click on Create User. Fill the form that pops up with the following:

  • Email: admin@example.com

  • Password and Repeat Password: Any password of your choice

  • Connection: Username-Password-Authentication

Click on the Create button. The page of the admin@example.com user loads up. On this page, click on the Roles tab and then click on the Assign Roles button.

From the dropdown, select the menu-admin role that you created earlier and click on the Assign button. Verify that the user has the permissions by clicking on the Permissions tab. If so, your admin user is all set up and ready to use.

Use Admin Access

Head back to the WAB Dashboard and sign out. Click on the Sign In button again and, this time, login in as the admin@example.com user.

As this user has the menu-admin role, the UI unlocks admin features. Open the WAB Dashboard Menu and notice the Add Item link at the upper-right corner. Click on a menu item and notice how you now can edit or delete the item.

However, at this moment, non-admin users could circumvent the client-side route protections to unlock the admin features of the UI. Additionally, they could extract the access token sent by Auth0 using a browser's developer tools and make requests directly to the server write endpoints using cUrl in the terminal.

Your server needs to implement role-based access control to mitigate these attack vectors.

Implement Role-Based Access Control in Express

To implement role-based access control (RBAC) in Express, you create an RBAC middleware function that inspects the access token provided in the client request and verifies that it has the permissions required by the endpoint it needs to access. You also must call this function before your application reaches the route handler function of the protected endpoint controller.

Consequently, if the proper permissions are present in the access token, your application grants the user access to the protected endpoint by calling the next middleware function in the chain. Otherwise, your application terminates the request-response cycle and sends a response with a 403 Forbidden status code to the client.

To help you inspect the access token for permissions easily, you will use the express-jwt-authz package:

npm install express-jwt-authz

Using this package, you will read the permisisons claim of the access token to determine if the client making a request to a protected endpoint has all the permissions required.

To start, create a file to define a middleware function that checks for permissions:

touch src/middleware/permissions.middleware.ts

Then, populate src/middleware/permissions.middleware.ts with the following code:

const jwtAuthz = require("express-jwt-authz");

export const checkPermissions = (permissions: string | string[]) => {
  return jwtAuthz([permissions], {
    customScopeKey: "permissions",
    checkAllScopes: true,
    failWithError: true
  });
};

The jwtAuthz function takes as a first argument an array of string representing the permissions required by an endpoint. Its second argument is an optional object, which is used to configure how jwtAuthz should behave by specifying different options as follows:

  • customScopeKey: By default, permissions are checked against a scope claim of the access token, but you can change it to be another claim name with this option. In this case, the permissions data is stored in a claim called permissions.

  • checkAllScopes: When set to true, all the expected permissions by the endpoint must be present in the customScopeKey claim of the access token. If any permission is missing, this middleware function throws an error, which effectively denies the client application making the request from accessing the protected endpoint.

  • failWithError: When set to true, jwtAuthz will forward any errors to the next() middleware function instead of ending the response directly. In this case, jwtAuthz will forward error to your errorHandler middleware function, where you can better customize the error response sent to the client.

jwtAuthz is a fully-defined and self-contained middleware function, which means it is a function that has access to the Request object, the Response object, and the next middleware function in the application’s request-response cycle. As such, you can technically avoid creating the checkPermissions helper function and invoke the jwtAuthz function directly on each endpoint as follows:

itemsRouter.post(
  "/",
  [
    checkJwt,
    jwtAuthz([ItemPermissions.CreateItems], {
      customScopeKey: "permissions",
      checkAllScopes: true,
      failWithError: true
    })
  ],
  async (req: Request, res: Response) => {
    // function body...
  }
);

itemsRouter.put(
  "/",
  [
    checkJwt,
    jwtAuthz([ItemPermissions.UpdateItems], {
      customScopeKey: "permissions",
      checkAllScopes: true,
      failWithError: true
    })
  ],
  async (req: Request, res: Response) => {
    // function body...
  }
);

However, this requires you to repeatedly configure jwtAuthz at each endpoint. Instead, you use a JavaScript closure to create a re-usable functional wrapper for jwtAuthz. The checkPermissions helper function takes as arguments the permissions required and creates a closure around that value within its body. It then returns an instance of jwtAuthz, which can access the value of permissions when it is executed by Express. As such, you only need to configure jwtAuthz once in one single place, making your code much more maintainable and less error-prone, which is the approach you are going to apply to the endpoints.

With the RBAC authorization middleware function created, you are now ready to wire it into any controller that needs role-based access control (RBAC).

Add Endpoint Protection to Express Based on Authorization

To make it easy to manage and use permissions in your code, you can define them using a TypeScript enum. Under the src/items directory, create the following file:

touch src/items/item-permissions.ts

Populate src/items/item-permissions.ts as follows:

export enum ItemPermissions {
  CreateItems = "create:items",
  UpdateItems = "update:items",
  DeleteItems = "delete:items"
}

A TypeScript enum lets you define a set of named constants, which documents what these constants do while also preventing you from introducing bugs in your application by mistyping them. Each constant represents one of the permissions that you created in the Auth0 dashboard.

Now to protect the write endpoints with RBAC, you need to inject the checkPermissions middleware function into a controller definition and pass it the permissions that the endpoint requires as arguments.

Open src/items/items.router.ts, locate the Required External Modules and Interfaces section, and add the following imports:

/**
 * Required External Modules and Interfaces
 */

import express, { Request, Response } from "express";
import * as ItemService from "./items.service";
import { Item } from "./item.interface";
import { Items } from "./items.interface";

import { checkJwt } from "../middleware/authz.middleware";
import { checkPermissions } from "../middleware/permissions.middleware";
import { ItemPermissions } from "./item-permissions";

Next, locate the Controller Definitions section and update the following controller definitions:

/**
 * Controller Definitions
 */

// GET items/ ...

// GET items/:id ...

// Mount authorization middleware

itemsRouter.use(checkJwt);

// POST items/

itemsRouter.post(
  "/",
  checkPermissions(ItemPermissions.CreateItems),
  async (req: Request, res: Response) => {
    try {
      const item: Item = req.body.item;

      await ItemService.create(item);

      res.sendStatus(201);
    } catch (e) {
      res.status(404).send(e.message);
    }
  }
);

// PUT items/

itemsRouter.put(
  "/",
  checkPermissions(ItemPermissions.UpdateItems),
  async (req: Request, res: Response) => {
    try {
      const item: Item = req.body.item;

      await ItemService.update(item);

      res.sendStatus(200);
    } catch (e) {
      res.status(500).send(e.message);
    }
  }
);

// DELETE items/:id

itemsRouter.delete(
  "/:id",
  checkPermissions(ItemPermissions.DeleteItems),
  async (req: Request, res: Response) => {
    try {
      const id: number = parseInt(req.params.id, 10);
      await ItemService.remove(id);

      res.sendStatus(200);
    } catch (e) {
      res.status(500).send(e.message);
    }
  }
);

Now that the authorization guards are in place, any attempt to create a new menu item directly using a non-admin access token results in failure. The checkPermissions() function injected in the POST items/ endpoint detects the absence of the required permissions and passes a 403 exception down the middleware chain, successfully preventing the application from invoking the route handler of this endpoint.

Remember that by mounting the checkJwt middleware function as a router-level middleware, you don't have to include it at the endpoint-level. With just one line of code, itemsRouter.use(checkJwt), you are protecting all the endpoints that follow it against invalid access tokens. Then, you call the checkPermissions middleware before the endpoint logic is reached to protect the endpoint against clients with invalid permissions.

You are effectively implementing Role-Based Access Control (RBAC) using a two-layer approach powered by Express middleware functions.

Sign out and sign back in as the admin user in the WAB Dashboard. Try to add a new item. The Add Item form is pre-loaded with some data to make this process easier for you. If you already created the salad item, try to create a coffee item with this data:

name: Coffee
price: 2.99
description: Woke
image: https://cdn.auth0.com/blog/whatabyte/coffee-sm.png

Click on that newly created item and notice that you can either edit or delete it. Try both operations.

WAB Dashboard showing a newly added menu item, coffee

What's Next

You have reached the end of the tutorial, for now. What other chapters should we add to this tutorial? I have a few ideas in mind for future chapters:

  • Deploying an Express application to Heroku and AWS.

  • Connecting an Express application to a MongoDB or PostgreSQL store.

  • Using GraphQL or gRPC with Express.

Let me know through our feedback section if these are chapters you'd like to read or what other content I should cover. Thank you for reading this far and happy engineering!