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
- Authenticate via browser and load
/docs/internal-apis. - Open DevTools > Network tab.
- Navigate to a sibling route (e.g.,
/docs/public-api). - Confirm
window.__Docusaurushydration 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
403responses. - 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.