Adding Aserto Authorization to React and Node app

Dec 16th, 2021

Roie Schwaber-Cohen avatar

Roie Schwaber-Cohen

Integration  |  

Authorization

react node app and aserto

An updated version of this guide is available here.

Overview

Aserto is a cloud-native authorization platform that allows you to avoid having to build your own access control solution and instead frees you up to focus on your core user experience. In this tutorial you will learn how to integrate the Aserto SDK in the context of a Node.js service (using Express.js) that will interact with a React application.

Before we get started, let’s discuss two of Aserto's major components: the Authorizer and the Control Plane.

The Authorizer is where authorization decisions get made. It is an open source authorization engine which uses Open Policy Agent (OPA) to compute a decision based on policy, user context and resource data. In this tutorial we’re going to use the hosted version of this authorizer.

The Control Plane manages the lifecycle of policies, user context, and resource data that are used by the authorizer. The control plane makes it easy to manage these artifacts centrally, and takes care of the details of synchronizing them to the Authorizer instance(s) deployed at the edge. More specifically, it manages:

  • Connections to external systems such as identity providers and source control systems
  • References to registered authorization policies
  • A user directory built from the identity providers its connected to
  • A centralized log of aggregated decisions made by the Authorizer

The policy

At the core of Aserto’s authorization model is an authorization policy, which we refer to simply as a Policy. Policies are authored in a textual language called Rego, defined as part of the Open Policy Agent (OPA) project in the Cloud Native Computing Foundation.

We define the access control rules we want to enforce in our policy - as opposed to our application code. This is what's known as the "Policy-as-Code" approach, where authorization logic is decoupled from application logic.

Policies are treated just like application code or infrastructure-as-code - they are stored and versioned in a git repository. We’re going to define and see the policy in action later in this tutorial.

What to expect

This post assumes you have a working knowledge of Javascript and that you are familiar with React.js and Node.js.

When you’ve completed this tutorial you'll have learned how to:

  1. Create a React application with authentication using oidc-react
  2. Set up a simple Express.js application with authentication middleware and define a protected route
  3. Create and modify a very simple authorization policy
  4. Integrate the Aserto Authorization Express.js SDK to enable fine grained access control
  5. Conditionally render UI elements based on user access

It should take about 30-45 minutes to complete this tutorial.

Prerequisites

To get started, you’re going to need:

  1. Node.JS installed on your machine
  2. Aserto account and credentials (if you don't have one, sign up here!)
  3. Your favorite code editor

To get started, let's add users to your Aserto directory. We'll need these users to test our application and authorization policy.

Add users to the Aserto directory

Since our application deals with user access, we're going to need users in our directory. Aserto lets us add identity providers and automatically syncs the users registered with those identity providers to the Aserto directory and authorizer. In this tutorial we’re going to use the Acmecorp Identity Provider, which simulates an identity provider with hundreds of users, each with their own set of roles and attributes.

Log in to your Aserto account. To add the Acmecorp identity provider, go to the Connections tab, and click “Add connection”.

add connection

From the dropdown, select “acmecorp”:

select acmecorp as identity provider

Name the provider (you can choose whatever name you want) and give it a description. Then, click “Add connection” to complete the process.

Review your users

Click on the “Users” panel. The users you’ll see have been imported from the identity provider Acmecorp into your directory. Let’s review a couple of users: Search the directory for Euan Garden and click on his user card.

Euan user card

You’ll see the following JSON object (shortened here for brevity):

{
  "id": "cirkzmrhzgmzos03mzm1ltqwngqtywy2ni1jnzdjzjezyte1zjgsbwxvy2fs",
  "enabled": true,
  "display_name": "Euan Garden",
  "email": "euang@acmecorp.com",
  ...
  "identities": {
    ...
  },
  "attributes": {
    "properties": {
      "department": "Sales Engagement Management",
      "manager": "2bfaa552-d9a5-41e9-a6c3-5be62b4433c8",
      "phone": "+1-804-555-3383",
      "title": "Salesperson"
    },
    "roles": [
      "acmecorp",
      "sales-engagement-management",
      "user",
      "viewer"
    ],
    ...
  },
  ...
}

Users in this identity provider have properties and roles associated with them. In this case, among other roles, Euan has the role of a viewer. If you search the Aserto directory for the user Kris Johnson and inspect her associated JSON object, you’ll see she has the role of admin. Later in this tutorial we will leverage these roles to allow the authorizer to make a decision as to which user will have access to a piece of sensitive information.

But first, we'll set up our React application. Let's get started!

React application setup

We’re going to build a very bare bones application for this tutorial. We’ll start by creating an application using the yarn react-app generator: In your terminal, execute the following command:

yarn create react-app aserto-react-demo


You can now cd into the newly created folder and start the app:

cd aserto-react-demo
yarn start

The familiar React logo should appear, indicating that the app is ready to go.

react logo

Adding OIDC dependencies

Now that we have a running React application, we'll continue by installing and then importing the required dependency - oidc-react

In your terminal, execute the following command:

yarn add oidc-react

The following environment variables are used to point your application to Aserto’s demo IDP, so that you don’t have to set one yourself. Create a file called .env and add the following:

REACT_APP_OIDC_DOMAIN=acmecorp.demo.aserto.com
REACT_APP_OIDC_CLIENT_ID=acmecorp-app
REACT_APP_OIDC_AUDIENCE=acmecorp-app
REACT_APP_API_ORIGIN=http://localhost:8080

Note: Make sure the .env file is added to the .gitignore file so that it is not checked in.

Open the file src/index.js and add the dependency:

import { AuthProvider } from "oidc-react";

Add the following configuration object:

const configuration = {
  authority: `https://${process.env.REACT_APP_OIDC_DOMAIN}/dex`,
  clientId: process.env.REACT_APP_OIDC_CLIENT_ID,
  autoSignIn: true,
  responseType: "id_token",
  scope: "openid profile email",
  redirectUri: window.location.origin,
  audience: process.env.REACT_APP_OIDC_AUDIENCE,
  onSignIn: () => {
    window.location.replace(window.location.origin);
  },
};

Next, we'll wrap the top level React Application component with the AuthProvider, and pass it the required configuration we created.

ReactDOM.render(
  <React.StrictMode>
    <AuthProvider {...configuration}>
      <App />
    </AuthProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

Note: When developing locally, make sure your application is running on port 3000 - other ports are not registered with the identify provider and will not work.

If your application is still running, you should see the following login window:

login window

Use the following user credentials to log in:

  • Email address: euang@acmecorp.com
  • Password: V@erySecre#t123!

After logging in, you should see the React logo again.

react logo

Add a stylesheet

We've created a stylesheet for this app that you can reference in your index.html file in the public folder. in the <head> section, add the following:

<link rel="stylesheet" href="https://aserto-remote-css.netlify.app/react-and-node-quickstart.css"/>

Next, we’ll build the app itself. Open the App.js file, and replace it’s contents with:

import React, { useEffect } from "react";
import { useAuth } from "oidc-react";

function App() {
  const auth = useAuth();
  const isAuthenticated = auth.userData?.id_token ? true : false;

  //If the user logs out, redirect them to the login page
  useEffect(() => {
    if (!auth.isLoading && !isAuthenticated) {
      auth.signIn();
    }
  });

  return (
    <div className="container">
      <div className="header">
        <div className="logo-container">
          <div className="logo"></div>
          <div className="brand-name"></div>
        </div>
      </div>

      <div className="user-controls">
        {isAuthenticated && (
          <>
            <div className="user-info">{auth.userData?.profile?.email}</div>
            <div className="seperator"></div>
            <div className="auth-button">
              <div onClick={() => auth.signOut("/")}>Log Out</div>
            </div>
          </>
        )}
        {!isAuthenticated && (
          <div className="auth-button">
            <div onClick={() => auth.signIn("/")}>Login</div>
          </div>
        )}
      </div>

      <div className="main">
        {isAuthenticated && (
          <>
            <div className="top-main">
              <div className="welcome-message">
                Welcome {auth.userData?.profile?.email}!
              </div>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

export default App;

Test the application

Let's test our application by logging in. If it's not already running, start your application by executing:

yarn start

If you haven't already, log in, using the following credentials:

  • Email address: euang@acmecorp.com
  • Password: V@erySecre#t123!
log in as euan

If everything works as expected, the following should be displayed.

logged in as euan


We can make sure that the application's authentication flow works by logging out and then logging back in.

Great! Our application authenticates with the Acmecorp IDP, and so we have our user's identity in hand. Next, we'll create the Express.js service which will host our protected resource and will communicate with the Aserto hosted authorizer to determine whether or not a logged in user has the permissions to access the protected resource based on the user's identity.

Service setup

To get started, let's create a new folder called service under the React application folder. cd into the folder and run:

yarn init -y
yarn add express express-jwt jwks-rsa cors express-jwt-aserto dotenv

To the .env file we created previously, we'll add the following:

JWKS_URI=https://acmecorp.demo.aserto.com/dex/keys
ISSUER=https://acmecorp.demo.aserto.com/dex
AUDIENCE=acmecorp-app

In the service folder, Create a file called api.js - that will be our server. To this file, we'll add the following dependencies:

require("dotenv").config();
const express = require("express");
const jwt = require("express-jwt");
const jwksRsa = require("jwks-rsa");
const cors = require("cors");
const app = express();

In the next section we define the middleware function which will call our identity provider to verify the validity of the JWT (and also enable CORS): Express.js will pass the call to the checkJwt middleware which will determine whether the JWT sent to it is valid or not. If it is not valid, Express.js will return a 403 (Forbidden) response.

//Paste after the dependencies

const checkJwt = jwt({
  // Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: process.env.JWKS_URI,
  }),

  // Validate the audience and the issuer
  audience: process.env.AUDIENCE,
  issuer: process.env.ISSUER,
  algorithms: ["RS256"],
});

Lastly, we set up a protected route which will use the checkJwt middleware:

// Enable CORS
app.use(cors());

// Protected API endpoint
app.get("/api/protected", checkJwt, function (req, res) {
  //send the response
  res.json({
    secretMessage: "Here you go, very sensitive information for ya!",
  });
});

// Launch the API Server at localhost:8080
app.listen(8080);

Awesome! our service will be listening on port 8080 and we set up a protected endpoint. In the next section we'll test this endpoint by updating our application to send a JWT token.

Update the application

To test this endpoint we're going to have to make sure the React app actually sends the authentication token to the server and requests the protected resources. To do that, we'll have to make some changes to the App.js file in our React app.

At the top of the file, modify the line:

import React, { useEffect } from "react";

to:

import React, { useEffect, useCallback, useState } from "react";

Then, find the following code block:

function App() {
  const auth = useAuth();
  const isAuthenticated = auth.userData?.id_token ? true : false;

And add the following code right after the definition for the isAuthenticated variable:

const [message, setMessage] = useState(false);
const accessSensitiveInformation = useCallback(async () => {
  try {
    if (!auth.isLoading) {
      const accessToken = auth.userData?.id_token;
      const sensitiveInformationURL = `${process.env.REACT_APP_API_ORIGIN}/api/protected`;
      const sensitiveDataResponse = await fetch(sensitiveInformationURL, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });

      try {
        const res = await sensitiveDataResponse.json();
        setMessage(res.secretMessage);
      } catch (e) {
        //In case no access is given, the response will return 403 and not return a JSON response
        setMessage(sensitiveDataResponse.status);
      }
    }
  } catch (e) {
    console.log(e.message);
  }
}, [auth.isLoading, auth.userData?.id_token]);

In this portion of the code we create a callback (which will be triggered by a button). The callback will first get our JWT token from the identity provider, using the auth object that is obtained from the useAuth hook. Then we perform the call to our service sending the authorization token as part of our request's headers (fetch).

Finally, we parse the JSON response from the server and set the state of the message variable: if the service returns a 403 Forbidden or a 401 Unauthorized errors, and the message “No access to sensitive information” will be shown. If no error is returned from the service, the user has access to the protected resource and the message will be shown.

Next we’ll update the main section of the app (in the div with the className main) to include the button that will trigger accessSensitiveInformation and an area to show the message. Replace the existing div with the class main section with the following:

<div className="main">
  {isAuthenticated && (
    <>
      <div className="top-main">
        <div className="welcome-message">
          Welcome {auth.userData?.profile?.email}!
        </div>
        <div>
          {!message && (
            <button
              className="primary-button"
              onClick={() => accessSensitiveInformation()}
            >
              Get Sensitive Resource
            </button>
          )}
          <div className="message-container">
            {message && message !== 403 && message !== 401 && (
              <>
                <div className="lottie"></div>
                <div className="message">{message}</div>
              </>
            )}
            {message && message === 401 && (
              <>
                <div className="sad-lottie"></div>
                <div className="message">
                  No access to sensitive information
                </div>
              </>
            )}
            {message && message === 403 && (
              <>
                <div className="sad-lottie"></div>
                <div className="message">
                  No access to sensitive information
                </div>
              </>
            )}
          </div>
        </div>
      </div>
    </>
  )}
</div>

Test the application

To run both your application and the server in parallel, add the npm-run-all dependency: cd into the project's root folder and run:

yarn add npm-run-all

Then, update the package.json in the root folder, and add the following to the scripts section:

"scripts": {
 ...
 "start:server": "node service/api.js",
 "start:all": "npm-run-all --parallel start start:server"
},

First, stop the application by hitting ctrl+c in the terminal where you previously started the application. To start both the application and the server, you can now run:

yarn start:all

Let's test our application by first logging out, then logging in again with the email euan@acmecrop.com and the password V@erySecre#t123!.

If everything works as expected, we should see the following:

euan sees sensitive data

We can further test this by intentionally sending a malformed header and making sure the sensitive information isn't shown. One way to do this is to append so rogue characters to the access token like so:

...
const sensitiveDataResponse = await fetch(sensitiveInformationURL, {
   headers: {
       Authorization: `Bearer ${accessToken}SOME_ROGUE_CHARACTERS`,
   },
});

In this case we'd expect the "No access to sensitive information" message to be shown.

euan has no access

Checkpoint

At this point, we have successfully implemented the authentication flow in our React application. We can now move on to the next step: creating an authorization policy that will govern how users access our protected endpoint.

Creating a role-based access control authorization model

Until now, we dealt only with authentication of users in our application. Now, let’s discuss how to set up the authorization model which will enforce some limitation on user access to our protected resource. We want to limit access to the protected resource in our application only to users who have particular roles.

As we saw before in the Aserto Directory, the user Euan (euang@acmecorp.com) has the viewer role, and the user Kris (krisj@acmecorp.com) has the admin role. Right now, if you log in as Euan user you’ll see the following:

euan has access

We want to ensure that if we're logged in as Euan (a viewer), the application won't allow access to our protected resource. Since we didn't add any way to authorize users based on their role - all users will have access to the protected resource. Let's fix that by first creating a simple Aserto policy to allow access only to users with the admin role. We'll then use this policy in our application.

Create an aserto policy

Initially, the policy we’ll create for this tutorial will only allow a user with the role of admin to access our protected resource, while users without the admin role will not be able to access that resource.

In console.aserto.com, go to the Policies tab and click "Add Policy"

add policy

If you haven't already added a source code connection, select "Add a new source code connection". You can choose either adding a GitHub connection using an OAuth2 flow, or add a GitHub connection using a GitHub PAT.

select source code provider

Once you've added the connection, select the organization you'd like to use for the repository, and select "New (using template)".

Then, from the template dropdown, select "aserto-dev/policy-template". Name repo "policy-aserto-react", and click "Create repo".

select organization and repo

Finally, name your policy "aserto-react" and click "Add Policy".

name policy and add policy

Aserto will generate a new repository in your GitHub account the will include the necessary policy files. Head to GitHub to retrieve the URL for the repository that was just created, and clone it.


git clone git@github.com:<YOUR ORGANIZATION>/hello-aserto-react.git

Now that we have a local copy of the policy, let’s start modifying it:

We'll start by updating the .manifest file under src, which currently will only point to the root of our policy. We'll change it from:

{
    "roots": ["policies"]
}

to:

{
    "roots": ["asertodemo"]
}

Rename the file hello.rego to protected.rego. We'll open the file and change the package name to match the path of our Express API endpoint. The basic structure of the package name is:

[policy-root].[VERB].[path]

Where the path is separated by dots instead of slashes. And so in our case, the Express.js path

app.get('/api/protected'...

Is referenced in the package as:

package asertodemo.GET.api.protected

We're also going to define the policy such that the only allowed user is one with an admin role. Aserto attaches this user object to the input object. Below is the finished policy:

package asertodemo.GET.api.protected

default allowed = false

allowed {
    some index
    input.user.attributes.roles[index] == "admin"
}

By default, the allowed decision is going to be false - this follows the principle of a “closed” system where access is disallowed unless specific conditions are satisfied.

At runtime, the application will send the JWT associated with the logged in user. The Express.js service will relay the JWT along with the request path as the identity and resource contexts respectively to the authorizer.

The some index and ...roles[index] expressions indicate the authorizer will iterate over all the elements in the roles array under the attributes property in the user object. The authorizer will check if the iterated role is equal to the string admin. If it is, the allowed decision will evaluate to true.

Updating the policy repository

Now that we’ve modified our policy, we’ll publish our changes: Aserto applies a GitOps flow to any changes made to the repository we set up. That means that we can simply tag and push the changes we’ve made to the policy and the updated policy will be built and published to the authorizer.

Commit, tag and push the changes you made:

git add .
git commit -m "updated policy"
git push
git tag v0.0.1
git push --tags

Open the Aserto console, and navigate to the Policies tab. Then, open the policy "policy-aserto-react" and review the changes. You should see the following:

policy in the console

Checkpoint

Great! We now have a policy that will only allow users with the admin role access our protected resource. In the next section we'll see how to reference the policy in our Express.js service.

Update the Express service to use the Aserto Express.js middleware

In order to have our policy govern authorization in our service, we need to configure and apply the Aserto Express.js middleware. In order to avoid saving any secret credentials in our source code, we'll add the following credentials to our .env file. To find these credentials, click on your policy in the Policies tab. Then choose the "Policy settings" tab.

policy details

Copy the following values to the .env file:

POLICY_ID={Your Policy ID}
AUTHORIZER_API_KEY={Your Authorizer API Key}
TENANT_ID={Your tenant ID}
POLICY_ROOT=asertodemo
AUTHORIZER_SERVICE_URL=https://authorizer.prod.aserto.com

Add the following dependency reference in service/api.js (after the const jwt = require("express-jwt"); line):

const { jwtAuthz } = require("express-jwt-aserto");

Continue by creating the configuration object for the Aserto middleware. Add the following section after the const app = express(); line:


const authzOptions = {
  authorizerServiceUrl: process.env.AUTHORIZER_SERVICE_URL,
  policyId: process.env.POLICY_ID,
  policyRoot: process.env.POLICY_ROOT,
  authorizerApiKey: process.env.AUTHORIZER_API_KEY,
  tenantId: process.env.TENANT_ID,
};

We'll define a function for the Aserto middleware, and pass it the configuration object.


//Aserto authorizer middleware function
const checkAuthz = jwtAuthz(authzOptions);


Lastly, add the checkAuthz middleware to our protected route: Add the reference to the checkAuthz middleware right after the checkJwt middleware reference. You endpoint definition should look like this:


//Protected API endpoint
app.get("/api/protected", checkJwt, checkAuthz, function (req, res) {
  //send the response
  res.json({ secret: "Very sensitive information presented here" });
});

The checkAuthz middleware is going to pass the request context - which consists of the policy reference (based on the request route), the identity context (based on the JWT token passed) and resource context (based on the request parameters) - to the authorizer, which given the policy will determine what the allowed decision would be.

Test the application

Before testing the application, stop both the application and the server by hitting ctrl+c and run yarn start:all again.

When we log in with the user krisj@acmecorp.com who has the role of an admin we will still be able to see the following:

krisj sees sensitive information

If we log out and log in again as euang@acmecorp.com we will see the following:

euan has no access

Euan doesn’t have the role of admin, and the route /api/protected will be disallowed.

Summary

This concludes this tutorial! We covered the basics of connecting a React application to an identity provider, using OIDC for authentication and setting up a Node.js service that verified the validity of the JWT token passed from the application. We then learned how to create and update a role-based access control authorization policy and how to use that policy with the Aserto Express.js middleware to create a protected endpoint that will allow access only to users with an authorized role.

But you're journey doesn't have to end here! You can learn how to expand your policy to include more roles, as well as how to use Aserto's React SDK for conditional UI rendering.

The finished application code is available in here.

Roie Schwaber-Cohen avatar

Roie Schwaber-Cohen

Developer Advocate