Adding Aserto Authorization to React and Node app
Dec 16th, 2021
Roie Schwaber-Cohen
Integration |
Authorization
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:
- 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 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”.
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",
...
"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.
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:
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 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!
If everything works as expected, the following should be displayed.
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:
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.
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:
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"
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.
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".
Finally, name your policy "aserto-react" and click "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:
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.
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:
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
, 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
Developer Advocate
Related Content
From RBAC to ABAC
Role-based access control is a powerful pattern for handling many authorization use-cases, but in some cases - a more nuanced approach may be needed. In this post, we'll explore Attribute-Based Access Control, an authorization model that allows us to leverage dynamic user attributes to make fine-grained authorization decisions.
Jan 20th, 2022
Building RBAC in Node
If you're looking to implement RBAC with Node.js, there are many options to choose from. In this post, we'll review some of the existing tools in the Node.js ecosystem.
Feb 3rd, 2022
Flask RBAC demystified: a developer's guide
Flask is one of Python's most popular web frameworks. It's lightweight, easy to master, and extensible. There are few, if any, applications that you can't tackle with Flask, including role-based access control (RBAC).
In this post, we'll look at setting up RBAC with Flask. We'll start with a simple Flask application and roll our own RBAC system.
Feb 24th, 2022