This post is part of “Authorization in microservices with Open Policy Agent, NodeJs, and ReactJs” series.

  1. Introduction
  2. Backend
  3. Frontend (this post)

Table of Contents:

Background

In the previous post, we built a NodeJs backend that protects its endpoint by checking the authorization against OPA api server.

In this post, we will explore another important type of software - Frontend.

Normally, the authorization logic is shared between backend and frontend. In frontend, we use authorization to decide how to show the UI to the user for a good user experience. In backend, we need to check the same authorization to make sure that no one can by pass it.

Although the authorization logic is shared, the code itself is hardly shared due to the fact that the frontend and backend code are separate. This causes the duplication of logic that we need to sync between code bases.

This demo, I will show you how to reuse the same authorization logic as backend and how to run it locally on the browser using WebAssembly.

Demo application components

This post will cover no. 5 in above picture.

Prerequisite

This demo requires these tools to be installed in your machine.

  • NodeJs + NPM

Introduce the demo application

This demo is a RejectJs application. When a user opens the UI, it will download OPA policy as WebAssembly’s wasm and data.json file from the bundle server. Then display buttons to the user, when clicked, the authorization is evaluated using the input shown on the UI and the result will show on the screen.

Prepare WebAssembly bundle

In the first post, we built a bundle the policy as tar file, this file is used by the backend. On the other hand, for the frontend, we need to build wasm file.

Go back to the bundle server project and run this command on the folder that contains policies files. (See first post)

mkdir -p public # make sure that the output folder exists

# build wasm bundle with default entry point to query "permission/allow"
opa build -t wasm -e permission/allow -o public/wasm.tar.gz -b .

# extract wasm bundle
tar -xf public/wasm.tar.gz -C public --exclude=.manifest

# Output files
# public/policy.wasm
# public/data.json

Note

The folder public/ is served by nginx http server we run in the first post.

Create a ReactJs project

We will create a simple ReactJs project using createReactApp.

npx create-react-app opa-ui
cd opa-ui
npm start

Create an OPA hook

We will use a react hook to download the files from the bundle server and provide a method to evaluate the authorization.

// src/useOpaWasm.js
import { loadPolicy } from "@open-policy-agent/opa-wasm";
import { useEffect, useState } from "react";

export default function useOpaWasm(baseUrl) {
  const [policy, setPolicy] = useState();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState();

  useEffect(() => {
    const load = async () => {
      const policyWasmResp = await fetch(`${baseUrl}/policy.wasm`, {
        cache: "no-cache",
      });
      const policyWasm = await policyWasmResp.arrayBuffer();
      const policy = await loadPolicy(policyWasm);

      const dataResp = await fetch(`${baseUrl}/data.json`, {
        cache: "no-cache",
      });
      const data = await dataResp.json();
      policy.setData(data);

      setPolicy(policy);
    };
    setLoading(true);
    load()
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [baseUrl]);

  return { policy, loading, error };
}

Note

This hook rely on library “@open-policy-agent/opa-wasm” to load and evaluate OPA wasm file. It uses “fetch” api to download policy.wasm and data.json files from the bundle server. “no-cache” means it will try to download the latest file in case that the rules might be updated.

Create the UI

We will create a UI that use the hook we’ve created.

// src/App.js
import { useState } from 'react';
import './App.css';
import useOpaWasm from './useOpaWasm';

function App() {
  // load wasm from the bundle server
  const { policy } = useOpaWasm("http://localhost:8182");

  // store evaluation result
  const [info, setInfo] = useState({ text: '', color: '', input: {} });

  // check permission against requested input
  const check = (role, action, object) => {
    const input = {
      subject: {
        roles: [role],
      },
      action,
      object,
    };
    const result = policy.evaluate(input);

    const allow = result[0].result;
    if (allow) {
      setInfo({ text: `As ${role} you can ${action} ${object}`, color: 'green', input });
    } else {
      setInfo({ text: `As ${role} you cannot ${action} ${object}`, color: 'red', input });
    }
  }

  return (
    <div className="App">
      <div style={{ color: info.color }}>{info.text}</div>
      <div><button onClick={() => check('admin', 'create', 'order')}>Can Admin create order?</button></div>
      <div><button onClick={() => check('admin', 'read', 'order')}>Can Admin read order?</button></div>
      <div><button onClick={() => check('user', 'create', 'order')}>Can User create order?</button></div>
      <div><button onClick={() => check('user', 'read', 'order')}>Can User read order?</button></div>
      <pre style={{textAlign: 'left'}}>input: {JSON.stringify(info.input, null, 4)}</pre>
    </div>
  );
}

export default App;

Note

When the button is clicked, it will call “check” method which will evaluate the authorization policy with the given input (user role, request action, request object). The authorization logic is based on the first post.

Example UI when the request is authorized

Example UI when the request is not authorized


Conclusion

Well done. You’ve learned the authorization concept in microservices environment. Create authorization policy using Open Policy Agent. Then, apply it to NodeJs backend and ReactJs frontend.

This knowledge should be a good start to apply it in real world.

Authorization is important for every applications. Doing it right and effective is crucial for the applications in the long run.

See the complete code here.

Thank you :D