EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
MeshCentral Custom Authentication Implementation Plan

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

  1. Build custom MeshCentral:
    cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral/meshcentral-fork
    npm install
  2. Update Dockerfile: Already done - pulls from your fork
  3. 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=
  4. Test locally:
    cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
    ./test-local.sh
  5. 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

  1. 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
  2. Rebuild Docker container:
    cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
    ./deploy.sh
  3. 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
  • Test direct embedding

Phase 3: Database Integration (2-3 hours)

  • Update config.json.template
  • Add PostgreSQL connection pool
  • Test user lookup queries

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
  • Test all tabs with JWT

Phase 5: Tenant Isolation (3-4 hours)

  • Implement domain mapping
  • Auto-create device groups
  • Test multi-tenant isolation

Phase 6: Testing (4-6 hours)

  • Local testing with test-local.sh
  • WebSocket connection tests
  • File transfer tests
  • Remote desktop tests
  • Multi-tenant tests

Phase 7: Deployment (1-2 hours)

  • Commit to fork
  • Deploy to DigitalOcean
  • Monitor logs
  • Production testing

Phase 8: Documentation (2-3 hours)

  • Document customizations
  • 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

  1. Review this plan - Confirm approach and timeline
  2. Set up PostgreSQL environment variables in MeshCentral config
  3. Create jwt-auth.js module
  4. Start Phase 1 - Core authentication modifications

Would you like me to start implementing Phase 1 now?