Building a Note/Thought Sharing WebApp with OPAL Authorization and Custom MySQL Fetcher



This content originally appeared on DEV Community and was authored by Sonu Singh

Integrating authorization using OPA (Open Policy Agent) with a custom MySQL fetcher in a Node.js Express application, and consuming it with a React frontend, involves several steps. Here’s a detailed walkthrough that covers setting up the backend, Docker environment, and frontend interactions. Below, I’ll structure the blog and include all the necessary components you requested.

Goal: Implementing Authorization with OPA and Custom MySQL Fetcher

In this tutorial, we’ll set up a Node.js Express backend with JWT authentication and OPA for authorization, utilizing a custom MySQL fetcher. Our frontend, built with React, will interact with this backend to perform CRUD operations on notes.

Prerequisites

Before we begin, make sure you have:

  • Node.js and npm installed
  • Docker installed (for local development environment setup)
  • Basic knowledge of React and Node.js

1. Setting up the Backend

Docker Compose Configuration (docker-compose.yml)
version: "3.8"
services:
  broadcast_channel:
    image: postgres
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

  adminer:
    image: adminer
    restart: always
    ports:
      - 8083:8080

  example_db:
    image: mysql:8.0
    cap_add:
      - SYS_NICE
    restart: always
    environment:
      - MYSQL_DATABASE=test
      - MYSQL_USER=test
      - MYSQL_PASSWORD=mysql
      - MYSQL_ROOT_PASSWORD=mysql
    logging:
      options:
        max-size: 10m
        max-file: "3"
    ports:
      - "3306:3306"
    volumes:
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql

  opal_server:
    image: permitio/opal-server
    environment:
      - OPAL_BROADCAST_URI=postgres://postgres@broadcast_channel:5432/postgres
      - UVICORN_NUM_WORKERS=4
      - OPAL_POLICY_REPO_URL=https://github.com/sonu2164/opal-example-policy-repo
      - OPAL_POLICY_REPO_POLLING_INTERVAL=30
      - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"mysql://root@example_db:3306/test?password=mysql","config":{"fetcher":"MySQLFetchProvider","query":"SELECT * FROM users","connection_params":{"host":"example_db","user":"root","port":3306,"db":"test","password":"mysql"}},"topics":["mysql"],"dst_path":"users"},{"url":"mysql://root@example_db:3306/test?password=mysql","config":{"fetcher":"MySQLFetchProvider","query":"SELECT * FROM notes","connection_params":{"host":"example_db","user":"root","port":3306,"db":"test","password":"mysql"}},"topics":["mysql"],"dst_path":"notes"}]}}
    ports:
      - "7002:7002"
    depends_on:
      - broadcast_channel

  opal_client:
    build:
      context: .
    environment:
      - OPAL_SERVER_URL=http://opal_server:7002
      - OPAL_LOG_FORMAT_INCLUDE_PID=true
      - OPAL_FETCH_PROVIDER_MODULES=opal_common.fetcher.providers,opal_fetcher_mysql.provider
      - OPAL_INLINE_OPA_LOG_FORMAT=http
      - OPA_LOG_LEVEL=debug
      - OPAL_SHOULD_REPORT_ON_DATA_UPDATES=True
      - OPAL_DATA_TOPICS=mysql
    ports:
      - "7766:7000"
      - "8181:8181"
    depends_on:
      - opal_server
      - example_db
    command: sh -c "./wait-for.sh opal_server:7002 example_db:3306 --timeout=20 -- ./start.sh"

Explanation:

  • Sets up PostgreSQL and MySQL databases for storage (broadcast_channel and example_db).
  • Initializes Opal Server (opal_server) with configurations to connect to PostgreSQL and MySQL databases.
  • Deploys Opal Client (opal_client) and connects it to Opal Server and MySQL database.

2. Backend Application (server/index.js)

const express = require("express");
const axios = require("axios");
const cors = require("cors");
const updateOpaData = require("./src/updata_opa_data").updateOpaData;
const noteshandler = require("./src/noteshandler");
const userhandler = require("./src/userhandler");
const app = express();
const port = 3001;
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

const jwt = require('jsonwebtoken');

app.use(express.json());
app.use(cors());

// Authentication middleware
const authenticate = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return res.status(401).json({ error: 'Authorization header missing' });
  }

  const token = authHeader.split(' ')[1]; // Bearer <token>

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await prisma.user.findUnique({
      where: {
        id: decoded.userId,
      },
    });

    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }

    req.user = user;
    next();
  } catch (error) {
    console.error('JWT Verification Error:', error);
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

// Authorization middleware
const authorization = async (req, res, next) => {
  try {
    const userId = parseInt(req.user.id);
    const noteId = parseInt(req.body.noteId);

    const check = await axios.post(
      "http://localhost:8181/v1/data/app/rbac/allow",
      {
        input: {
          user: userId,
          note: noteId,
        },
      }
    );

    if (check.data.result) {
      next(); // Proceed to the next middleware or route handler
    } else {
      res.status(403).json({ message: "Unauthorized" });
    }
  } catch (error) {
    console.error("Error during authorization check:", error);
    res.status(500).json({ message: "Internal Server Error" });
  }
};

// Routes
app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.post("/user/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await userhandler.loginUser(email, password);
  return res.status(200).json(user);
});

app.post("/user/register", async (req, res) => {
  const { name, email, password, role } = req.body
  const user = await userhandler.registerUser(name, email, password, role);
  await updateOpaData();
  return res.status(200).json(user);
});

app.get("/notes", async (req, res) => {
  const notes = await noteshandler.notes();
  return res.status(200).json(notes);
});

app.post("/newNote", authenticate, async (req, res) => {
  const noteObj = {
    title: req.body.title,
    description: req.body.description,
    user: { connect: { id: req.user.id } },
  };

  try {
    const note = await noteshandler.newNote(noteObj);
    await updateOpaData(); // Update OPA data after creating a new note
    return res.status(200).json(note);
  } catch (error) {
    console.error("Error creating new note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.post("/note/update", authenticate, authorization, async (req, res) => {
  const { noteId, title, description } = req.body;

  try {
    const note = await noteshandler.updateNote(noteId, title, description);
    await updateOpaData(); // Update OPA data after updating a note
    return res.status(200).json(note);
  } catch (error) {
    console.error("Error updating note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.post("/note/delete", authenticate, authorization, async (req, res) => {
  const { noteId } = req.body;
  try {
    const delRes = await noteshandler.deleteNote(noteId);
    await updateOpaData(); // Update OPA data after deleting a note
    return res.status(200).json({ status: delRes });
  } catch (error) {
    console.error("Error deleting note:", error);
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

app.listen(port, () => {
  console.log(`App server listening on port ${port}`);
});

Explanation:

  • Sets up Express server with middleware for JSON parsing, CORS handling, JWT authentication, and authorization.
  • Defines routes for user authentication (/user/login, /user/register), note CRUD operations (/notes, /newNote, /note/update, /note/delete).
  • Uses JWT tokens for authentication and checks authorization using OPA.
  • Utilizes Axios to make HTTP requests to OPA server (opal_server) for authorization checks.

3. Frontend Integration (React)

To interact with these backend endpoints from a React frontend, you can use Axios or Fetch API for making HTTP requests. Here’s a basic example:

Check the endpoints using postman

Head over to github repo for reference

https://github.com/sonu2164/keeper_opal_auth_quira_submission
*See readme in repo to run the project *


This content originally appeared on DEV Community and was authored by Sonu Singh