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
andexample_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