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

Overview

This document describes the custom JWT authentication system implemented for integrating MeshCentral with the RMM+PSA platform. The system enables true Single Sign-On (SSO) by using JWT tokens from the dashboard to authenticate MeshCentral sessions, eliminating the need for separate logins.

Implementation Date

  • Started: February 15, 2026
  • Completed: February 16, 2026
  • Production Deployment: February 16, 2026

Architecture

High-Level Flow

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ │ │ │ │ │
│ RMM Dashboard │────────▶│ Backend API │────────▶│ MeshCentral Fork │
│ (React SPA) │ JWT │ (Node.js) │ JWT │ (Node.js) │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
│ │ │
│ │ │
│ ▼ ▼
│ ┌──────────────────┐ ┌──────────────────┐
│ │ PostgreSQL │◀────────│ PostgreSQL │
└──────────────────▶│ (User DB) │ │ (Connection) │
localStorage └──────────────────┘ └──────────────────┘

Components

  1. Dashboard (React)
  2. Backend API (Node.js)
    • Issues JWT tokens on user login
    • Validates JWT tokens on API requests
    • Proxies authenticated requests to MeshCentral
    • Endpoint: POST /meshcentral/session/:agentId
  3. MeshCentral Fork
    • Custom JWT authentication module (jwt-auth.js)
    • Modified authentication handlers in webserver.js
    • Validates JWT tokens against PostgreSQL user database
    • Creates MeshCentral sessions from JWT claims
  4. PostgreSQL Database
    • Shared user database
    • Tables: users, agents, meshcentral_sessions
    • Tenant isolation via tenant_id field

JWT Token Format

Token Claims

{
"userId": 123,
"user_id": 123,
"email": "user@company.com",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"role": "admin",
"iat": 1708057200,
"exp": 1708143600
}

Token Generation

Location: /home/cw/Documents/IBG_HUB/rmm-psa-backend/middleware/auth.js

const jwt = require('jsonwebtoken');
const token = jwt.sign(
{
userId: user.user_id,
email: user.email,
tenantId: user.tenant_id
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

Token Validation

Two secrets are supported for flexibility:

  1. JWT_SECRET - Primary secret for dashboard tokens
  2. AGENT_SIGN_KEY - Secondary secret for agent tokens

Both secrets can validate tokens (OR logic).

File Changes

New Files

1. jwt-auth.js (370 lines)

Location: /opt/meshcentral/jwt-auth.js

Purpose: Core JWT authentication module

Key Functions:

  • CreateJWTAuth(parent) - Factory function
  • verifyToken(token) - JWT signature validation
  • validateToken(token, callback) - Full user validation with DB lookup
  • getUserByEmail(email) - PostgreSQL user query
  • getUserDeviceLinks(userId, tenantId) - Device access mapping
  • ensureTenantMesh(tenantId, tenantName) - Auto-create device groups
  • extractToken(req) - Extract JWT from multiple sources
  • healthCheck() - Database connectivity check

Dependencies:

{
"jsonwebtoken": "^9.0.2",
"pg": "^8.11.3"
}

PostgreSQL Connection:

new Pool({
host: process.env.POSTGRES_HOST || process.env.DB_HOST,
port: parseInt(process.env.POSTGRES_PORT || process.env.DB_PORT || '25060'),
database: process.env.POSTGRES_DB || process.env.DB_NAME || 'defaultdb',
user: process.env.POSTGRES_USER || process.env.DB_USER || 'doadmin',
password: process.env.POSTGRES_PASSWORD || process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: false },
max: 10
})

User Mapping:

// PostgreSQL user → MeshCentral user
{
_id: `user//${email}`,
name: full_name || email.split('@')[0],
email: email,
domain: '',
siteadmin: is_superadmin ? 0xFFFFFFFF : 0,
links: deviceLinks,
tenantId: tenant_id
}

Modified Files

2. meshcentral.js

Location: /opt/meshcentral/meshcentral.js Line: ~1926

Change:

// Initialize JWT Auth if enabled
if (config.settings.jwtAuth) {
obj.jwtAuth = require('./jwt-auth').CreateJWTAuth(obj);
obj.jwtAuth.init();
}

3. webserver.js (4 modifications)

Location: /opt/meshcentral/webserver.js

Modification 1 - Root Request Handler (Line ~2907-3020)

Added JWT authentication before cookie-based auth:

} else if (req.query.token && obj.parent.jwtAuth) {
// JWT token authentication (RMM+PSA Integration)
var jwtToken = req.query.token;
obj.parent.jwtAuth.validateToken(jwtToken, function (jwtUser) {
if (jwtUser) {
parent.debug('web', 'handleRootRequestEx: JWT auth ok for ' + jwtUser._id);
delete req.session.loginmode;
req.session.userid = jwtUser._id;
delete req.session.currentNode;
req.session.ip = req.clientIp;
setSessionRandom(req);
if (!obj.users[jwtUser._id]) {
obj.users[jwtUser._id] = jwtUser;
}
handleRootRequestExAuthenticated(req, res, domain, direct);
} else {
handleRootRequestExAuthenticated(req, res, domain, direct);
}
});
return;
}

Modification 2 - MeshAction Endpoint (Line ~5964)

JWT fallback when cookie auth fails:

// If no cookie auth, try JWT authentication (RMM+PSA Integration)
if (user == null && obj.parent.jwtAuth) {
const token = obj.parent.jwtAuth.extractToken(req);
if (token) {
// Validate JWT synchronously (simplified)
obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
user = jwtUser;
}
});
}
}

Modification 3 - HTTP JWT Middleware (Line ~6689)

Auto-authenticate API requests:

// JWT Authentication Middleware (RMM+PSA Integration)
obj.app.use(function (req, res, next) {
// Skip JWT auth for specific public routes
if (req.path.startsWith('/agent.ashx') ||
req.path.startsWith('/meshrelay.ashx') ||
req.path === '/ws' ||
req.path === '/control.ashx') {
return next();
}
if (obj.parent.jwtAuth) {
const token = obj.parent.jwtAuth.extractToken(req);
if (token) {
obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
req.session = req.session || {};
req.session.userid = jwtUser._id;
req.session.ip = req.clientIp;
obj.parent.debug('web', `JWT HTTP Auth: ${jwtUser._id}`);
}
next();
});
return;
}
}
next();
});

Modification 4 - WebSocket Authentication (Line ~7045)

Authenticate WebSocket connections:

// Check for JWT token authentication (RMM+PSA Integration)
if (obj.parent.jwtAuth) {
const token = obj.parent.jwtAuth.extractToken(req);
if (token) {
obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
if (jwtUser) {
obj.parent.debug('web', `JWT authenticated user: ${jwtUser._id}`);
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws, req, obj.args, domain, jwtUser, authData);
} else {
// JWT invalid, fall back to normal auth
obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws, req, obj.args, domain, user, authData);
}
});
return;
}
}

4. package.json

Location: /opt/meshcentral/package.json

Dependencies Added:

{
"dependencies": {
"jsonwebtoken": "^9.0.2",
"pg": "^8.11.3"
}
}

5. Backend - routes/meshcentral.js

Location: /home/cw/Documents/IBG_HUB/rmm-psa-backend/routes/meshcentral.js Endpoint: POST /meshcentral/session/:agentId

Changes:

  • Removed MeshCentral API client dependency
  • Extract JWT from Authorization header
  • Pass JWT directly to MeshCentral via ?token= parameter
  • Store JWT (not cookie) in session tracking

Before:

const api = await getTenantMeshAPI(agent.tenant_id);
const meshcentralUrl = `${MESHCENTRAL_URL}/?login=${api.cookie}`;

After:

const authHeader = req.headers.authorization;
const jwtToken = authHeader.substring(7); // Remove 'Bearer '
const meshcentralUrl = `${MESHCENTRAL_URL}/?token=${encodeURIComponent(jwtToken)}`;

Configuration

Environment Variables

MeshCentral (DigitalOcean App Platform)

# JWT Authentication
JWT_SECRET=aFzl7P9UDEgc2hK7xLXGVj5tI1vTDsyRXSOEdBj55wA=
AGENT_SIGN_KEY=5bf7e505c65d07dd60178b92b0751a0eecb3947269b95b837a85bd5075d446c4
# PostgreSQL Database
POSTGRES_HOST=rmm-psa-db-do-user-28531160-0.i.db.ondigitalocean.com
POSTGRES_PORT=25060
POSTGRES_USER=doadmin
POSTGRES_PASSWORD=AVNS_J8RJAmsEwsHFG52_-F2
POSTGRES_DB=defaultdb
# Fallback DB variables (for compatibility)
DB_HOST=${POSTGRES_HOST}
DB_PORT=${POSTGRES_PORT}
DB_USER=${POSTGRES_USER}
DB_PASSWORD=${POSTGRES_PASSWORD}
DB_NAME=${POSTGRES_DB}

Backend API

JWT_SECRET=aFzl7P9UDEgc2hK7xLXGVj5tI1vTDsyRXSOEdBj55wA=
DATABASE_URL=postgresql://doadmin:password@host:25060/defaultdb
MESHCENTRAL_URL=https://rmm-psa-meshcentral-aq48h.ondigitalocean.app

MeshCentral Config

File: /opt/meshcentral/meshcentral-data/config.json.template

{
"settings": {
"jwtAuth": true,
"postgres": {
"host": "${POSTGRES_HOST}",
"port": ${POSTGRES_PORT},
"user": "${POSTGRES_USER}",
"password": "${POSTGRES_PASSWORD}",
"database": "${POSTGRES_DB}"
}
},
"domains": {
"": {
"title": "RMM+PSA Remote Management",
"newAccounts": false,
"certUrl": "${MESHCENTRAL_URL}:443"
}
}
}

Authentication Flow

1. User Login to Dashboard

User → Dashboard → Backend API
POST /auth/login
{email, password}
Backend → PostgreSQL
SELECT * FROM users WHERE email = ?
Backend → User
{token: "eyJhbGc...", user: {...}}
Dashboard
localStorage.setItem('token', token)

2. Open Remote Desktop Tab

User clicks RDS tab → TabRds.jsx
TabRds → Backend API
POST /meshcentral/session/115
Authorization: Bearer eyJhbGc...
Backend → PostgreSQL
SELECT * FROM agents WHERE agent_id = 115
Backend → User
{iframeUrl: "https://mesh.../?token=eyJhbGc...&gotonode=..."}
MeshCentralEmbed
<iframe src={iframeUrl} />

3. MeshCentral Authenticates iframe

iframe loads → MeshCentral webserver.js
handleRootRequest()
├─ Checks req.query.token
├─ Calls jwtAuth.validateToken()
│ ├─ Verifies JWT signature
│ ├─ Queries PostgreSQL users table
│ ├─ Maps user to MeshCentral format
│ └─ Returns jwtUser object
├─ Creates req.session.userid
├─ Stores user in obj.users[]
└─ Continues to authenticated page
User sees MeshCentral interface (no login prompt)

4. WebSocket Connection (Terminal/Files)

Dashboard → WebSocket ws://mesh.../meshrelay.ashx
?token=eyJhbGc...&node=...
MeshCentral webserver.js
├─ WebSocket upgrade handler
├─ Extracts token from query
├─ Validates JWT
├─ Creates MeshUser
└─ Establishes relay connection
Terminal/Files interface works seamlessly

Security Considerations

Token Storage

  • JWT stored in localStorage (XSS risk)
  • httpOnly cookies recommended for production
  • Token rotation every 24 hours

Token Validation

  • Signature verification (HS256)
  • Expiration check
  • Database user lookup (can be revoked)
  • Tenant isolation enforced

Network Security

  • All traffic over HTTPS/WSS
  • MeshCentral certificate validation
  • PostgreSQL SSL enabled

Database Security

  • Connection pooling (max 10)
  • Prepared statements (SQL injection prevention)
  • Tenant_id isolation on all queries

Rate Limiting

  • Consider adding rate limits to JWT validation
  • Implement token blacklist for logout

Troubleshooting

Common Issues

1. "JWT Auth: PostgreSQL connection failed"

Symptoms: MeshCentral logs show connection errors

Solutions:

# Check environment variables
doctl apps logs 0ceb0932-3fa7-4a42-9a51-f0a124360a04 --type run | grep -i postgres
# Verify credentials
psql "postgresql://doadmin:password@host:25060/defaultdb?sslmode=require"
# Check firewall rules
# DigitalOcean DB → Trusted Sources → Add MeshCentral app IP

2. "Invalid or expired session"

Symptoms: Terminal/Files tabs show 401 errors

Solutions:

# Check JWT token expiration
echo "eyJhbGc..." | cut -d'.' -f2 | base64 -d | jq .exp
# Verify JWT_SECRET matches between backend and MeshCentral
# Backend:
grep JWT_SECRET /home/cw/Documents/IBG_HUB/rmm-psa-backend/.env
# MeshCentral:
doctl apps spec get 0ceb0932-3fa7-4a42-9a51-f0a124360a04 | grep JWT_SECRET

3. "Agent not connected to MeshCentral"

Symptoms: RDS tab shows "Agent not connected"

Solutions:

-- Check agent's meshcentral_nodeid
SELECT agent_id, hostname, meshcentral_nodeid
FROM agents
WHERE agent_id = 115;
-- Verify agent is online in MeshCentral
-- Check MeshCentral admin panel → Devices

4. iframe shows login page

Symptoms: MeshCentral login page visible in iframe

Solutions:

// Check if token is being passed
console.log(iframeUrl);
// Should see: ?token=eyJhbGc...
// Check MeshCentral logs for JWT validation
doctl apps logs 0ceb0932-3fa7-4a42-9a51-f0a124360a04 | grep "JWT auth"
// Verify jwt-auth.js is loading
doctl apps logs 0ceb0932-3fa7-4a42-9a51-f0a124360a04 | grep "JWT Auth: PostgreSQL"

Debug Logging

Enable verbose JWT logging:

MeshCentral:

// In jwt-auth.js, add debug logging
console.log('[JWT] Token:', token.substring(0, 20) + '...');
console.log('[JWT] Decoded:', decoded);
console.log('[JWT] User from DB:', user);

Backend:

// In routes/meshcentral.js
console.log('🔍 JWT token:', jwtToken.substring(0, 20) + '...');
console.log('🔍 Agent:', agent);
console.log('🔍 Generated URL:', meshcentralUrl);

Performance

Metrics

  • Authentication Time: ~50ms (JWT validation + DB query)
  • Session Creation: ~100ms (total request time)
  • WebSocket Setup: ~200ms (including TLS handshake)
  • User Cache TTL: 5 minutes (reduces DB queries)

Optimization Opportunities

  1. Redis Cache: Cache user lookups
    // Cache key: jwt:user:{email}
    // TTL: 5 minutes
  2. Connection Pooling: Already implemented (max 10)
  3. Prepared Statements: Consider using pg-promise
  4. Token Refresh: Implement refresh tokens (7-day expiry)

Testing

Manual Testing Checklist

  • Login to dashboard with valid credentials
  • JWT token stored in localStorage
  • Navigate to Devices → Select Agent → RDS tab
  • MeshCentral iframe loads without login prompt
  • Desktop view shows agent screen
  • Terminal tab connects without errors
  • Files tab shows file browser
  • Logout clears token
  • Expired token redirects to login

Automated Testing

Backend API Tests:

cd /home/cw/Documents/IBG_HUB/rmm-psa-backend
npm test -- meshcentral

Integration Tests:

// test/integration/meshcentral-jwt.test.js
describe('MeshCentral JWT Authentication', () => {
it('should create session with valid JWT', async () => {
const response = await request(app)
.post('/meshcentral/session/115')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(response.body.iframeUrl).toContain('?token=');
});
it('should reject expired JWT', async () => {
const response = await request(app)
.post('/meshcentral/session/115')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});

Deployment

Steps

  1. Deploy Backend:
    cd /home/cw/Documents/IBG_HUB/rmm-psa-backend
    git add routes/meshcentral.js
    git commit -m "Update MeshCentral JWT authentication"
    git push
    # Auto-deploys via GitHub Actions
  2. Deploy MeshCentral:
    cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral/meshcentral-fork
    git add jwt-auth.js webserver.js meshcentral.js package.json
    git commit -m "Add JWT authentication support"
    git push
    cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
    git add Dockerfile .do-app-spec.yaml
    git commit -m "Update deployment config for JWT auth"
    git push
    doctl apps create-deployment 0ceb0932-3fa7-4a42-9a51-f0a124360a04 --force-rebuild
  3. Verify Deployment:
    # Check MeshCentral logs
    doctl apps logs 0ceb0932-3fa7-4a42-9a51-f0a124360a04 --follow
    # Look for:
    # ✅ JWT Auth: PostgreSQL connected
    # JWT Authentication Module Initialized
  4. Test in Production:

Rollback Plan

If issues occur:

# Rollback backend
cd /home/cw/Documents/IBG_HUB/rmm-psa-backend
git revert HEAD
git push
# Rollback MeshCentral
cd /home/cw/Documents/IBG_HUB/rmm-psa-meshcentral
git revert HEAD
git push
doctl apps create-deployment 0ceb0932-3fa7-4a42-9a51-f0a124360a04 --force-rebuild

Maintenance

Regular Tasks

  • Weekly: Monitor error logs for JWT validation failures
  • Monthly: Review and rotate JWT secrets
  • Quarterly: Update MeshCentral fork from upstream
  • Annually: Security audit of JWT implementation

Monitoring

Key Metrics:

  • JWT validation success rate
  • Session creation time
  • WebSocket connection failures
  • Database connection pool utilization

Alerts:

  • PostgreSQL connection failures
  • JWT validation error rate > 5%
  • Session creation time > 500ms

Future Enhancements

  1. Refresh Tokens: Implement long-lived refresh tokens
  2. Redis Cache: Cache user lookups and device links
  3. Multi-Region: Support geographic load balancing
  4. OAuth2: Support third-party OAuth providers
  5. Hardware Keys: Support WebAuthn for 2FA
  6. Audit Logging: Comprehensive session audit trail

References

Support


Document Version: 1.0
Last Updated: February 16, 2026
Maintained By: IBG Development Team