Overview
This document outlines the plan to integrate JWT-based tenant authentication from your PostgreSQL database into the forked MeshCentral instance, enabling true single sign-on (SSO) without iframes or duplicate logins.
Current Architecture (MeshCentral Default)
User Browser → MeshCentral Login Page → MeshCentral User DB (NeDB/MongoDB)
↓
Passport.js Auth
↓
Session Cookie
↓
WebSocket Connections
Target Architecture (Custom JWT)
User Browser → Your Dashboard (JWT token in localStorage)
↓
React Components (Terminal, Files, RDS)
↓
WebSocket to MeshCentral with JWT header
↓
Custom JWT Validator → PostgreSQL User Lookup
↓
Validated Session → MeshCentral Functions
Phase 1: Core Authentication Modifications
1.1 Create Custom JWT Authentication Module
File: meshcentral-fork/jwt-auth.js (NEW)
Purpose: Validate JWT tokens from your backend and map to MeshCentral user objects.
Implementation:
// jwt-auth.js
const jwt = require('jsonwebtoken');
const { Pool } = require('pg');
module.exports.CreateJWTAuth = function (parent) {
const obj = {};
// PostgreSQL connection pool
obj.pool = new Pool({
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
});
// Validate JWT token
obj.validateToken = function (token, callback) {
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return callback(null);
// Token is valid, fetch user from PostgreSQL
obj.getUserByEmail(decoded.email, decoded.tenant_id, callback);
});
};
// Fetch user from PostgreSQL and map to MeshCentral format
obj.getUserByEmail = async function (email, tenantId, callback) {
try {
const result = await obj.pool.query(
'SELECT user_id, email, name, role, tenant_id FROM users WHERE email = $1 AND tenant_id = $2',
[email, tenantId]
);
if (result.rows.length === 0) return callback(null);
const pgUser = result.rows[0];
// Map PostgreSQL user to MeshCentral user format
const meshUser = {
_id: `user/${tenantId}/${pgUser.user_id}`,
email: pgUser.email,
name: pgUser.name,
domain: tenantId,
siteadmin: pgUser.role === 'admin' ? 0xFFFFFFFF : 0,
links: {} // Device links will be populated dynamically
};
callback(meshUser);
} catch (err) {
parent.debug('jwt-auth', 'PostgreSQL query error:', err);
callback(null);
}
};
return obj;
};
Files to modify:
- meshcentral.js - Initialize JWT auth module
- webserver.js - Add JWT validation middleware
1.2 Modify WebSocket Authentication Handler
File: meshcentral-fork/webserver.js
Lines to modify: ~6993-7001 (WebSocket connection handler)
Current Code:
if (user == null) { // User is not authenticated, perform inner server authentication
PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) {
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData);
}); // User is authenticated
}
New Code:
if (user == null) {
// Check for JWT token in query or headers
const token = req.query.token || req.headers['authorization']?.replace('Bearer ', '');
if (token && obj.jwtAuth) {
obj.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
// JWT authenticated successfully
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws, req, obj.args, domain, jwtUser, authData);
} else {
try { ws.close(); } catch (ex) { } // Invalid JWT
}
});
} else {
// Fallback to original authentication
PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) {
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData);
});
}
}
1.3 Add HTTP API JWT Authentication
File: meshcentral-fork/webserver.js
Purpose: Allow REST API calls with JWT tokens (for file downloads, etc.)
Add after line 6649 (passport patch):
// JWT Authentication Middleware
obj.app.use(function (req, res, next) {
// Skip JWT auth for public routes
if (req.path === '/login' || req.path === '/health') return next();
// Check Authorization header
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) return next();
const token = authHeader.replace('Bearer ', '');
if (obj.jwtAuth) {
obj.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
req.user = jwtUser;
req.session = req.session || {};
req.session.userid = jwtUser._id;
req.session.domainid = jwtUser.domain;
}
next();
});
} else {
next();
}
});
Phase 2: Frontend Integration Modifications
2.1 Remove Login Page Requirement
File: meshcentral-fork/webserver.js
Purpose: Skip login page when JWT is provided
Modify HTTP GET handler for '/':
// Around line 1100 - main page handler
obj.app.get('/', function (req, res) {
// Check for JWT token
const token = req.query.token || req.headers['authorization']?.replace('Bearer ', '');
if (token && obj.jwtAuth) {
obj.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
// Auto-login with JWT
req.session.userid = jwtUser._id;
req.session.domainid = jwtUser.domain;
req.user = jwtUser;
// Redirect to requested view or default
const viewMode = req.query.viewmode || 10;
const nodeId = req.query.node || req.query.gotonode;
if (nodeId) {
return res.redirect(`/?viewmode=${viewMode}&node=${nodeId}`);
}
}
});
}
// Original handler continues...
});
2.2 Bypass Session-Based Cookie Authentication
File: meshcentral-fork/webserver.js
Purpose: Allow WebSocket connections without session cookies when JWT is provided
Modify: Lines 5958-5960 (cookie authentication check)
// Check if the cookie authenticates a user
// OR check for JWT token
const token = req.query.token || req.headers['authorization']?.replace('Bearer ', '');
if (req.session && req.session.userid) {
// Cookie auth
// ... existing code
} else if (token && obj.jwtAuth) {
// JWT auth
obj.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
req.user = jwtUser;
req.session = req.session || {};
req.session.userid = jwtUser._id;
req.session.domainid = jwtUser.domain;
}
});
}
Phase 3: Database Integration
3.1 Add PostgreSQL Connection Configuration
File: meshcentral-fork/config.json.template
Add section:
{
"settings": {
"postgres": true,
"jwtAuth": true
},
"postgres": {
"host": "${POSTGRES_HOST}",
"port": ${POSTGRES_PORT},
"database": "${POSTGRES_DB}",
"user": "${POSTGRES_USER}",
"password": "${POSTGRES_PASSWORD}"
},
"jwt": {
"secret": "${JWT_SECRET}"
}
}
3.2 Initialize PostgreSQL Pool in Main Server
File: meshcentral-fork/meshcentral.js
Add after NeDB initialization (around line 200):
// Initialize JWT Auth if enabled
if (config.settings.jwtAuth) {
obj.jwtAuth = require('./jwt-auth').CreateJWTAuth(obj);
console.log('JWT Authentication enabled with PostgreSQL backend');
}
Phase 4: React Dashboard Integration
4.1 Update WebSocket Connection with JWT
File: rmm-psa-dashboard/src/pages/tabs/TabTerminal.jsx
Already implemented! Your current code passes the token:
const token = localStorage.getItem('token');
const ws = new WebSocket(
`${protocol}://${window.location.host}/api/meshcentral/terminal?nodeId=${nodeId}&token=${token}`
);
✅ No changes needed - backend proxy will forward token to MeshCentral
4.2 Remove Iframe Login (TabRds.jsx)
File: rmm-psa-dashboard/src/pages/tabs/TabRds.jsx
Current: Uses iframe that requires MeshCentral login Target: Direct embed with JWT passed via query parameter
// Instead of iframe with MeshCentral login URL
const meshUrl = `${MESHCENTRAL_URL}/?token=${token}&node=${nodeId}&viewmode=11`;
<iframe
src={meshUrl}
style={{ width: '100%', height: '800px', border: 'none' }}
title="Remote Desktop"
/>
Phase 5: Tenant Isolation
5.1 Map Tenant ID to MeshCentral Domain
File: meshcentral-fork/jwt-auth.js
Modify getUserByEmail function:
// Use tenant_id as MeshCentral domain
const meshUser = {
_id: `user/${tenantId}/${pgUser.user_id}`,
email: pgUser.email,
name: pgUser.name,
domain: tenantId, // Tenant becomes domain
siteadmin: pgUser.role === 'admin' ? 0xFFFFFFFF : 0,
links: {}
};
5.2 Auto-Create Device Groups Per Tenant
File: meshcentral-fork/jwt-auth.js
Add function:
obj.ensureTenantMesh = async function (tenantId, callback) {
const meshId = `mesh/${tenantId}/default`;
// Check if mesh exists
parent.db.Get(meshId, function (err, mesh) {
if (mesh && mesh.length > 0) return callback(mesh[0]);
// Create new mesh for tenant
const newMesh = {
_id: meshId,
name: `Tenant ${tenantId} Devices`,
mtype: 2, // Managed devices
desc: `Auto-created device group for tenant ${tenantId}`,
domain: tenantId
};
parent.db.Set(newMesh, function () {
callback(newMesh);
});
});
};
Phase 6: Testing & Deployment
6.1 Local Testing Setup
- Build custom MeshCentral:
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral/meshcentral-fork
npm install
- Update Dockerfile: Already done - pulls from your fork
- Add environment variables to .env:
POSTGRES_HOST=rmm-psa-db-do-user-28531160-0.i.db.ondigitalocean.com
POSTGRES_PORT=25060
POSTGRES_DB=defaultdb
POSTGRES_USER=doadmin
POSTGRES_PASSWORD=AVNS_J8RJAmsEwsHFG52_-F2
JWT_SECRET=aFzl7P9UDEgc2hK7xLXGVj5tI1vTDsyRXSOEdBj55wA=
- Test locally:
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
./test-local.sh
- Test JWT authentication:
# Get JWT token from your dashboard
TOKEN="your-jwt-token-here"
# Test WebSocket connection
wscat -c "ws://localhost:443/meshrelay.ashx?token=$TOKEN&nodeid=your-node-id"
6.2 Deployment to DigitalOcean
- Commit changes to fork:
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral/meshcentral-fork
git add .
git commit -m "Add JWT authentication with PostgreSQL backend"
git push origin main
- Rebuild Docker container:
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
./deploy.sh
- Monitor deployment:
doctl apps logs <app-id> --follow
Phase 7: Upstream Sync Strategy
7.1 Set Up Upstream Remote
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral/meshcentral-fork
git remote add upstream https://github.com/Ylianst/MeshCentral.git
git fetch upstream
7.2 Monthly Merge Strategy
# Create merge branch
git checkout -b merge-upstream-$(date +%Y-%m)
# Merge upstream changes
git merge upstream/master
# Resolve conflicts (focus on preserving jwt-auth.js and auth modifications)
# Test thoroughly
# Push to main after testing
git checkout main
git merge merge-upstream-$(date +%Y-%m)
git push origin main
7.3 Security Monitoring
Implementation Checklist
Phase 1: Core Authentication (5-8 hours)
- Add HTTP API JWT middleware
- Update meshcentral.js initialization
Phase 2: Frontend Integration (2-3 hours)
- Remove login page requirement
- Bypass session cookie checks
Phase 3: Database Integration (2-3 hours)
- Update config.json.template
- Add PostgreSQL connection pool
Phase 4: React Dashboard Updates (2-3 hours)
- Verify TabTerminal token passing (✅ already done)
- Verify TabFiles token passing (✅ already done)
- Update TabRds to remove iframe login
Phase 5: Tenant Isolation (3-4 hours)
- Auto-create device groups
- Test multi-tenant isolation
Phase 6: Testing (4-6 hours)
- Local testing with test-local.sh
- WebSocket connection tests
Phase 7: Deployment (1-2 hours)
Phase 8: Documentation (2-3 hours)
- Create upstream merge guide
- Write troubleshooting guide
Estimated Total Time
20-35 hours depending on complexity and testing thoroughness
Potential Issues & Solutions
Issue 1: PostgreSQL Connection Lag
Problem: Database queries slow down WebSocket connections Solution: Implement user caching with 5-minute TTL
Issue 2: JWT Token Expiration
Problem: Token expires mid-session Solution: Implement token refresh endpoint in backend
Issue 3: MeshCentral Session State
Problem: MeshCentral expects session objects Solution: Create synthetic session objects from JWT data
Issue 4: Device Links Not Populating
Problem: User doesn't have device links Solution: Query PostgreSQL agents table and populate links dynamically
Next Steps
- Review this plan - Confirm approach and timeline
- Set up PostgreSQL environment variables in MeshCentral config
- Create jwt-auth.js module
- Start Phase 1 - Core authentication modifications
Would you like me to start implementing Phase 1 now?