Building a React and Node app with Aserto authorization
Overview
Aserto is a cloud-native authorization platform that saves you from having to build your own access control solution, freeing 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 review two major components of Aserto: the Authorizer and the Control Plane.
The Authorizer is where authorization decisions are made. It is an open-source authorization engine which uses Open Policy Agent (OPA) to compute decisions based on policy, user context, and resource data. In this tutorial we’re going to use the hosted version of the 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 of your application. 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 it is 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’ll 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:
- Create a React application with authentication using
oidc-react
- Set up a simple Express.js application with authentication middleware and define a protected route
- Create and modify a very simple authorization policy
- Integrate the Aserto Authorization Express.js SDK to enable fine grained access control
- 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:
- Node.JS installed on your machine
- Aserto account and credentials (if you don't have one, sign up here!)
- Your favorite code editor
To begin, 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”.
From the dropdown, select “acmecorp”:
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.
You’ll see the following JSON object (shortened here for brevity):
{
"id": "cirkzmrhzgmzos03mzm1ltqwngqtywy2ni1jnzdjzjezyte1zjgsbwxvy2fs",
"enabled": true,
"display_name": "Euan Garden",
"email": "euang@acmecorp.com",
"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
Note: If you'd like to skip to the end, rather than follow the steps, simply clone the demo repo:
git clone git@github.com:aserto-demo/aserto-react-and-node-with-conditional-rendering.git
cd aserto-react-and-node-with-conditional-rendering
yarn install:all
From here, make sure you consult the README
. Specifically, you also need to instantiate an Aserto policy before you can run yarn start:all
to run the app. To do that, skip to the step called "Create an Aserto policy".
Create a new React app
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.
Downgrade to React 17
The OIDC dependencies we will shortly install aren't yet working with React 18, so we'll need to downgrade.
yarn add react@17 react-dom@17 @testing-library/react@12 @types/react@17 @types/react-dom@17
You'll also need to change index.js
as follows:
Change the line import { createRoot }
from 'react-dom/client';
to the following:
import ReactDOM from 'react-dom';
Next, change this block:
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
To the following:
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Make sure the app still loads and shows the spinning React logo.
We are now back to React v17.0.2 and can proceed to add authentication and authorization.
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@1.5.1
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 this 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.
Add a stylesheet
We've created a stylesheet for this app. To reference it 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!
If everything works as expected, you should see this:
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 @aserto/aserto-node 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 { expressjwt: 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 this 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 this 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 403 Forbidden
or 401 Unauthorized
errors, the message “No access to sensitive information” will be shown. If no error is returned from the service, the user will get access to the protected resource.
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 euang@acmecrop.com
and the password V@erySecre#t123!
.
If everything works as expected, we should see the following:
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 some 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 displayed.
Checkpoint
At this point, we have successfully implemented an 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 (RBAC) authorization model
Until now, we dealt only with authentication of users in our application. We focused on ensuring that our users are who they claim to be. Now, we can move on to setting-up an authorization model which will enforce limitations on user access to protected resources. In this example, we want to limit access to the protected resource in our application only to users who have particular roles. This authorization model is called role-based access control (RBAC) and it is the simplest model to set up.
Earlier we saw that the user Euan Garden(euang@acmecorp.com
) has the viewer
role, and the user Kris Johnson (krisj@acmecorp.com
) has the admin
role. Right now, if you log in as Euan you’ll see the following:
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. Users without the admin
role will not be able to access that resource.
In console.aserto.com, go to the Policies
tab and click the "Create an instance" button:
Choose "...from a sample image", and select policy-aserto-react
from the dropdown. Choose the latest
tag, and use the name aserto-react
.
Finally, click the "Create policy" button.
That was easy! We now have a policy that will only allow users with the admin
role access our protected resource.
You can examine the policy by clicking on the Modules tab in the left-nav.
package asertodemo.GET.api.protected
default allowed = false
allowed {
some index
input.user.properties.roles[index] == "admin"
}
In this policy, the only allowed
user is one with an admin
role.
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 click the "Download config" button in the top right, and add the values in the downloaded file to your .env
file:
Copy the values from the downloaded file to your .env
file:
ASERTO_POLICY_INSTANCE_NAME=aserto-react
ASERTO_POLICY_INSTANCE_LABEL=aserto-react
ASERTO_TENANT_ID={Your Tenant ID}
ASERTO_AUTHORIZER_API_KEY={Your Authorizer API Key}
ASERTO_AUTHORIZER_SERVICE_URL=authorizer.prod.aserto.com:8443
ASERTO_POLICY_ROOT=asertodemo
Add the following dependency reference in service/api.js
(after the const jwt = require("express-jwt");
line):
const { jwtAuthz } = require("@aserto/aserto-node");
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.ASERTO_AUTHORIZER_SERVICE_URL,
instanceName: process.env.ASERTO_POLICY_INSTANCE_NAME,
instanceLabel: process.env.ASERTO_POLICY_INSTANCE_LABEL,
policyRoot: 'asertodemo',
authorizerApiKey: process.env.ASERTO_AUTHORIZER_API_KEY,
tenantId: process.env.ASERTO_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. Your 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:
If we log out and log in again as euang@acmecorp.com
we will see the following:
Euan doesn’t have the role of admin
, so the route /api/protected
will be disallowed.
Summary
This concludes our 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 allows 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.
As always, we'd love to hear your feedback. Drop us a line here, or join our community Slack.
Related Content
Building RBAC in Go
If you're looking to implement RBAC with Go, there are several options to choose from. In this post, we'll review some of the existing tools in the Go ecosystem.
Aug 12th, 2024
Adding Authorization to a Go app with Topaz
In this tutorial, we'll learn how to add authorization to a Todo app written in Go, using the Aserto Go SDK.
Aug 20th, 2024
Using Identity Information from Azure Active Directory in Aserto
We are excited to offer Azure Active Directory users a seamless connection with the Aserto authorization platform. They can now target users and groups from Azure AD in authorization policies and enforce fine-grained controls. They can also manage all their policies in one place with Aserto to simplify policy management.
Apr 5th, 2023