Implementing fine-grained RBAC in Docusaurus

Docusaurus generates static sites by default, which complicates dynamic access control. For internal developer portals, platform engineers must enforce route-level permissions without sacrificing build performance. This guide details a production-ready middleware pattern that intercepts requests, validates JWT role claims, and maps them to Docusaurus route patterns. When designing your broader Authentication, RBAC & Security Governance strategy, static site generators require a proxy or serverless edge layer to evaluate permissions before serving content.

Context: Why Static Sites Need Dynamic Interception

Docusaurus compiles Markdown and React components into static HTML/JS bundles. Traditional client-side routing cannot securely enforce permissions because the source files are publicly accessible. Fine-grained access control requires evaluating identity tokens at the network edge or via a lightweight Node.js wrapper before the static assets are delivered. This approach aligns with standard Role-Based Access Control Setup practices, ensuring that route evaluation happens server-side while preserving Docusaurus’s fast client-side navigation post-authentication.

Exact Configuration: Express Middleware with JWT Role Mapping

Deploy a lightweight Express server to proxy requests to the Docusaurus build/ directory. The middleware extracts the Authorization header, verifies the JWT signature, and parses custom role claims. Route patterns are matched against a permission matrix. If the user lacks the required role, the server returns a 403 or redirects to an access-denied page.

1. Install Dependencies

npm init -y
npm install express express-jwt

2. Implement RBAC Proxy (server.js)

const express = require('express');
const { expressjwt: jwtCheck } = require('express-jwt');
const app = express();

const routePermissions = {
 '/docs/internal-apis': ['platform-engineer', 'tech-lead'],
 '/docs/security-audit': ['security-engineer', 'tech-lead'],
 '/admin/*': ['admin']
};

app.use(jwtCheck({
 secret: process.env.JWT_SECRET,
 algorithms: ['RS256'],
 requestProperty: 'auth'
}));

app.use((req, res, next) => {
 const userRoles = req.auth.payload.roles || [];
 const requestedPath = req.path;
 
 const requiredRoute = Object.keys(routePermissions).find(route => {
 if (route.endsWith('/*')) return requestedPath.startsWith(route.slice(0, -2));
 return requestedPath === route;
 });

 if (requiredRoute) {
 const hasAccess = routePermissions[requiredRoute].some(role => userRoles.includes(role));
 if (!hasAccess) return res.status(403).json({ error: 'Insufficient RBAC privileges' });
 }
 next();
});

// Serve static Docusaurus build output
app.use(express.static('build'));
app.listen(3000, () => console.log('Docusaurus RBAC proxy active'));

3. Nginx Edge Proxy (Optional)

If deploying behind an existing reverse proxy, offload token validation to an auth subrequest:

location ~ ^/docs/internal-apis {
 auth_request /validate-token;
 error_page 401 403 = /access-denied.html;
 proxy_pass http://localhost:3000;
}

location = /validate-token {
 internal;
 proxy_pass http://localhost:8080/verify;
 proxy_pass_request_body off;
 proxy_set_header Content-Length "";
 proxy_set_header X-Original-URI $request_uri;
}

4. Rapid Deployment

# Build Docusaurus
npm run build

# Start RBAC proxy in production
NODE_ENV=production JWT_SECRET=$(cat .env.jwt) node server.js

Validation: Testing Role Enforcement

Verify enforcement by issuing test JWTs with varying roles arrays. Use curl to request protected routes and confirm HTTP status codes.

Unauthorized Access Test

curl -s -o /dev/null -w "%{http_code}" \
 -H "Authorization: Bearer <token_without_platform-engineer_role>" \
 http://localhost:3000/docs/internal-apis
# Expected: 403

Authorized Access Test

curl -s -o /dev/null -w "%{http_code}" \
 -H "Authorization: Bearer <token_with_platform-engineer_role>" \
 http://localhost:3000/docs/internal-apis
# Expected: 200

SPA Hydration Check

  1. Authenticate via browser and load /docs/internal-apis.
  2. Open DevTools > Network tab.
  3. Navigate to a sibling route (e.g., /docs/public-api).
  4. Confirm window.__Docusaurus hydration does not bypass the initial server-side gate. Subsequent client-side transitions should respect the session token injected during the first request.

Edge Cases & Mitigations

  • Static Asset Caching Leaks: CSS/JS/images may expose metadata if served without authentication headers. Configure the proxy to explicitly bypass auth checks for /assets/, /img/, and /js/ paths.
  • SSR Hydration Mismatches: Initial HTML differing from the client bundle causes hydration errors. Ensure the middleware serves a consistent fallback UI or identical DOM structure for 403 responses.
  • Wildcard Route Over-Granting: /docs/* patterns can inadvertently grant access to sibling documentation paths. Implement regex-based permission evaluation (/^\/docs\/(?!public)/) to prevent over-permissive grants.
  • Session Persistence Risks: Long-lived tokens extend unauthorized access windows. Enforce short-lived JWTs (15–30 minutes) with automatic refresh rotation at the IdP level.

Common Pitfalls & Rollback

  • Client-Side Only Guards: Relying solely on React routing guards allows direct URL access to static bundles. Always enforce server-side validation.
  • Over-Permissive Wildcards: Broad route definitions grant access to unintended paths. Audit regex boundaries before deployment.
  • Unfiltered Asset Paths: Failing to exclude static directories breaks CSS/JS rendering. Whitelist known asset prefixes.
  • Stale Token Sessions: Using long-lived JWTs without refresh rotation extends privilege escalation windows.

Rapid Rollback Procedure

If middleware introduces latency or routing failures:

# 1. Stop RBAC proxy
pkill -f "node server.js"

# 2. Fallback to direct static serving
npx serve build -l 3000

# 3. Revert configuration
git checkout HEAD -- server.js
git commit -m "Revert RBAC middleware due to routing regression"

Frequently Asked Questions

Can Docusaurus enforce RBAC natively without an external proxy? No. Docusaurus is a static site generator. All route protection must occur at the network edge, via a reverse proxy, or through a serverless function that intercepts requests before serving the build output.

How do I handle client-side navigation after initial authentication? Once the initial HTML is served with a valid session cookie or token, Docusaurus handles subsequent SPA navigation client-side. You can inject a lightweight JS guard that checks the token before rendering protected components, but server-side validation remains mandatory for the first request.

What happens if a user’s role changes mid-session? Static sessions will not reflect role updates until the next full page load or token refresh. Implement short-lived JWTs (15-30 minutes) and force re-authentication on sensitive route transitions to ensure real-time permission alignment.