2 * JWT Authentication Module for MeshCentral
3 * Integrates with PostgreSQL user database for tenant-based authentication
5 * This module validates JWT tokens from the RMM+PSA backend and maps users
6 * to MeshCentral's internal user format, enabling true SSO.
11module.exports.CreateJWTAuth = function (parent) {
13 const jwt = require('jsonwebtoken');
14 const { Pool } = require('pg');
16 // Log what environment variables we actually receive
17 console.log('[JWT Auth] Environment variables check:');
18 console.log(' JWT_SECRET:', process.env.JWT_SECRET ? `${process.env.JWT_SECRET.substring(0, 20)}...` : 'NOT SET');
19 console.log(' AGENT_SIGN_KEY:', process.env.AGENT_SIGN_KEY ? `${process.env.AGENT_SIGN_KEY.substring(0, 20)}...` : 'NOT SET');
20 console.log(' POSTGRES_HOST:', process.env.POSTGRES_HOST ? 'SET' : 'NOT SET');
21 console.log(' POSTGRES_PASSWORD:', process.env.POSTGRES_PASSWORD ? 'SET' : 'NOT SET');
24 obj.jwtSecret = process.env.JWT_SECRET;
25 obj.agentSignKey = process.env.AGENT_SIGN_KEY;
27 // PostgreSQL connection pool
28 // NOTE: jwt-auth connects to user database (defaultdb), NOT MeshCentral's device database
29 // Use DB_NAME first (for users), fallback to POSTGRES_DB only if DB_NAME not set
30 const dbHost = process.env.POSTGRES_HOST || process.env.DB_HOST;
31 const dbPort = parseInt(process.env.POSTGRES_PORT || process.env.DB_PORT || '25060');
32 const dbName = process.env.DB_NAME || process.env.POSTGRES_DB || 'defaultdb';
33 const dbUser = process.env.POSTGRES_USER || process.env.DB_USER || 'doadmin';
34 const dbPassword = process.env.POSTGRES_PASSWORD || process.env.DB_PASSWORD;
36 console.log('[JWT Auth] Database config:', {
37 host: dbHost ? dbHost.substring(0, 20) + '...' : 'NOT SET',
41 passwordSet: !!dbPassword
44 if (!dbHost || !dbPassword) {
45 console.error('❌ JWT Auth: Missing required database environment variables');
46 console.error('Required: POSTGRES_HOST/DB_HOST and POSTGRES_PASSWORD/DB_PASSWORD');
47 throw new Error('Missing database configuration');
56 ssl: { rejectUnauthorized: false },
57 connectionTimeoutMillis: 5000,
58 idleTimeoutMillis: 30000,
62 // User cache to reduce database queries (5 minute TTL)
63 obj.userCache = new Map();
64 obj.cacheTimeout = 5 * 60 * 1000; // 5 minutes
66 // Initialize and log configuration
67 obj.init = function () {
68 parent.debug('jwt-auth', 'JWT Authentication Module Initialized');
69 parent.debug('jwt-auth', `PostgreSQL: ${obj.pool.options.host}:${obj.pool.options.port}/${obj.pool.options.database}`);
71 // Test database connection
72 obj.pool.query('SELECT NOW()', (err, result) => {
74 parent.debug('jwt-auth', 'PostgreSQL connection failed:', err.message);
75 console.error('❌ JWT Auth: PostgreSQL connection failed:', err.message);
77 parent.debug('jwt-auth', 'PostgreSQL connection successful');
78 console.log('✅ JWT Auth: PostgreSQL connected');
84 * Validate JWT token and return decoded payload
85 * Tries JWT_SECRET first, then AGENT_SIGN_KEY (for agent tokens)
87 obj.verifyToken = function (token, callback) {
88 if (!token) return callback(null, null);
90 // Try JWT_SECRET first (dashboard tokens)
91 jwt.verify(token, obj.jwtSecret, (err, decoded) => {
93 parent.debug('jwt-auth', `Token verified with JWT_SECRET for user: ${decoded.email || decoded.user_id}`);
94 return callback(null, decoded);
97 // Try AGENT_SIGN_KEY (agent tokens)
98 if (obj.agentSignKey) {
99 jwt.verify(token, obj.agentSignKey, (err2, decoded2) => {
101 parent.debug('jwt-auth', `Token verified with AGENT_SIGN_KEY for agent: ${decoded2.agent_uuid || decoded2.agentId}`);
102 return callback(null, decoded2);
105 parent.debug('jwt-auth', 'Token verification failed:', err.message, err2.message);
106 return callback(new Error('Invalid token'), null);
109 parent.debug('jwt-auth', 'Token verification failed:', err.message);
110 return callback(err, null);
116 * Validate JWT token and fetch corresponding user from PostgreSQL
117 * Returns MeshCentral-formatted user object
119 obj.validateToken = function (token, callback) {
120 console.log('[JWT Auth] validateToken called with token:', token ? token.substring(0, 30) + '...' : 'null');
122 obj.verifyToken(token, (err, decoded) => {
123 if (err || !decoded) {
124 console.log('[JWT Auth] Token verification failed:', err ? err.message : 'no decoded payload');
125 return callback(null);
128 console.log('[JWT Auth] Token decoded successfully:', JSON.stringify(decoded, null, 2));
130 // Extract user identifier - could be email, user_id, or userId
131 const email = decoded.email;
132 const userId = decoded.user_id || decoded.userId;
133 const tenantId = decoded.tenant_id || decoded.tenantId;
135 if ((!email && !userId) || !tenantId) {
136 console.log('[JWT Auth] Invalid token payload - missing user identifier or tenant_id', { email, userId, tenantId });
137 return callback(null);
141 const cacheKey = `${email || userId}_${tenantId}`;
142 const cached = obj.userCache.get(cacheKey);
143 if (cached && (Date.now() - cached.timestamp < obj.cacheTimeout)) {
144 parent.debug('jwt-auth', `Cache hit for user: ${cacheKey}`);
145 console.log('[JWT Auth] Cache hit for user:', cacheKey);
146 return callback(cached.user);
149 // Fetch from database - prefer email lookup, fall back to user_id
150 console.log('[JWT Auth] Looking up user in database:', { email, userId, tenantId });
152 const lookupFunction = email ? obj.getUserByEmail : obj.getUserById;
153 const lookupValue = email || userId;
155 lookupFunction.call(obj, lookupValue, tenantId, (meshUser) => {
157 console.log('[JWT Auth] User found and mapped:', meshUser._id);
159 obj.userCache.set(cacheKey, {
161 timestamp: Date.now()
164 console.log('[JWT Auth] User not found in database');
172 * Fetch user from PostgreSQL and map to MeshCentral user format
174 obj.getUserByEmail = async function (email, tenantId, callback) {
176 const result = await obj.pool.query(
186 WHERE email = $1 AND tenant_id = $2`,
190 if (result.rows.length === 0) {
191 parent.debug('jwt-auth', `User not found: ${email} in tenant ${tenantId}`);
192 return callback(null);
195 const pgUser = result.rows[0];
197 // Determine site admin privileges
198 // Root tenant (tenant_id = 1 or specific UUID) gets full admin
199 // Regular admins get limited admin rights
201 if (pgUser.role === 'admin') {
202 // Check if root tenant
203 if (tenantId === '1' || tenantId === '00000000-0000-0000-0000-000000000001') {
204 siteadmin = 0xFFFFFFFF; // Full admin
206 siteadmin = 0x00000006; // Manage users + server update
210 // Map PostgreSQL user to MeshCentral user format
211 // Use empty domain string "" for default domain
213 _id: `user//${email}`, // MeshCentral format: user/{domain}/{username}
215 name: pgUser.name || pgUser.email.split('@')[0],
216 domain: "", // Default domain (empty string)
217 siteadmin: siteadmin,
218 emailVerified: true, // Assume emails are verified in your system
219 creation: Math.floor(new Date(pgUser.created_at).getTime() / 1000),
221 // Custom fields for RMM+PSA integration
222 _external: true, // Mark as externally authenticated
223 _postgres_user_id: pgUser.user_id,
227 parent.debug('jwt-auth', `Mapped user: ${meshUser._id} (${meshUser.email}) siteadmin=${siteadmin}`);
228 console.log(`[JWT Auth] User mapped: ${meshUser._id}, siteadmin=${siteadmin}`);
230 // Fetch device links for this user's tenant
231 obj.getUserDeviceLinks(pgUser.user_id, tenantId, (links) => {
232 meshUser.links = links;
237 parent.debug('jwt-auth', 'PostgreSQL query error:', err.message);
238 console.error('❌ JWT Auth: Database query failed:', err.message);
244 * Fetch user by user_id from PostgreSQL and map to MeshCentral user format
246 obj.getUserById = async function (userId, tenantId, callback) {
248 console.log('[JWT Auth] getUserById:', { userId, tenantId });
250 const result = await obj.pool.query(
260 WHERE user_id = $1 AND tenant_id = $2`,
264 if (result.rows.length === 0) {
265 parent.debug('jwt-auth', `User not found: ID ${userId} in tenant ${tenantId}`);
266 console.log(`[JWT Auth] User not found: ID ${userId} in tenant ${tenantId}`);
267 return callback(null);
270 const pgUser = result.rows[0];
271 console.log('[JWT Auth] Found user in database:', { userId: pgUser.user_id, email: pgUser.email, name: pgUser.name, role: pgUser.role });
273 // Determine site admin privileges
275 if (pgUser.role === 'admin') {
276 // Check if root tenant
277 if (tenantId === '1' || tenantId === '00000000-0000-0000-0000-000000000001') {
278 siteadmin = 0xFFFFFFFF; // Full admin
280 siteadmin = 0x00000006; // Manage users + server update
284 // Map PostgreSQL user to MeshCentral user format
285 // Use empty domain string "" for default domain
286 const email = pgUser.email || `user${pgUser.user_id}@local`;
288 _id: `user//${email}`, // MeshCentral format: user/{domain}/{username}
290 name: pgUser.name || email.split('@')[0],
291 domain: "", // Default domain (empty string)
292 siteadmin: siteadmin,
294 creation: Math.floor(new Date(pgUser.created_at).getTime() / 1000),
296 // Custom fields for RMM+PSA integration
298 _postgres_user_id: pgUser.user_id,
302 parent.debug('jwt-auth', `Mapped user by ID: ${meshUser._id} (${meshUser.email}) siteadmin=${siteadmin}`);
303 console.log(`[JWT Auth] User mapped by ID: ${meshUser._id}, siteadmin=${siteadmin}`);
305 // Fetch device links for this user's tenant
306 obj.getUserDeviceLinks(pgUser.user_id, tenantId, (links) => {
307 meshUser.links = links;
312 parent.debug('jwt-auth', 'PostgreSQL query error (getUserById):', err.message);
313 console.error('❌ JWT Auth: Database query failed (getUserById):', err.message);
319 * Fetch device links for user's tenant from agents table
320 * Returns MeshCentral-compatible device links
322 obj.getUserDeviceLinks = async function (userId, tenantId, callback) {
324 const result = await obj.pool.query(
334 WHERE tenant_id = $1 AND meshcentral_nodeid IS NOT NULL`,
339 const meshId = `mesh/${tenantId}/default`; // Default mesh for tenant
341 result.rows.forEach(agent => {
342 if (agent.meshcentral_nodeid) {
343 // Link user to device
344 links[agent.meshcentral_nodeid] = { rights: 0xFFFFFFFF }; // Full rights
347 if (!links[meshId]) {
348 links[meshId] = { rights: 0xFFFFFFFF }; // Full rights to mesh
353 parent.debug('jwt-auth', `Found ${result.rows.length} devices for tenant ${tenantId}`);
357 parent.debug('jwt-auth', 'Failed to fetch device links:', err.message);
363 * Ensure tenant mesh exists, create if not
364 * Each tenant gets their own device group
366 obj.ensureTenantMesh = function (tenantId, callback) {
367 const meshId = `mesh/${tenantId}/default`;
369 parent.db.Get(meshId, function (err, meshes) {
370 if (meshes && meshes.length > 0) {
371 parent.debug('jwt-auth', `Mesh exists: ${meshId}`);
372 return callback(meshes[0]);
375 // Create new mesh for tenant
378 name: `Tenant ${tenantId} Devices`,
379 mtype: 2, // Managed mesh
380 desc: `Auto-created device group for tenant ${tenantId}`,
386 parent.db.Set(newMesh);
387 parent.debug('jwt-auth', `Created mesh: ${meshId}`);
388 console.log(`✅ JWT Auth: Created mesh for tenant ${tenantId}`);
395 * Clear user cache (useful for debugging or after user updates)
397 obj.clearCache = function (email, tenantId) {
398 if (email && tenantId) {
399 const cacheKey = `${email}_${tenantId}`;
400 obj.userCache.delete(cacheKey);
401 parent.debug('jwt-auth', `Cleared cache for ${cacheKey}`);
403 obj.userCache.clear();
404 parent.debug('jwt-auth', 'Cleared entire user cache');
409 * Extract JWT token from various sources
410 * Checks: Authorization header, query parameter, cookie
412 obj.extractToken = function (req) {
413 // Check Authorization header (Bearer token)
414 if (req.headers && req.headers.authorization) {
415 const parts = req.headers.authorization.split(' ');
416 if (parts.length === 2 && parts[0] === 'Bearer') {
421 // Check query parameter
422 if (req.query && req.query.token) {
423 return req.query.token;
426 // Check cookie (from WebSocket upgrade request)
427 if (req.headers && req.headers.cookie) {
428 const cookies = req.headers.cookie.split(';');
429 for (let i = 0; i < cookies.length; i++) {
430 const cookie = cookies[i].trim();
431 if (cookie.startsWith('jwt=')) {
432 return cookie.substring(4);
441 * Authenticate user with username/password against PostgreSQL
442 * Returns MeshCentral user object if successful, null otherwise
444 obj.authenticatePassword = async function (username, password, callback) {
446 console.log('[JWT Auth] Password authentication attempt for:', username);
448 // Query user by email or username
449 const result = await obj.pool.query(
460 WHERE (email = $1 OR name = $1)`,
461 [username.toLowerCase()]
464 if (result.rows.length === 0) {
465 console.log('[JWT Auth] User not found:', username);
466 return callback(null);
469 const pgUser = result.rows[0];
471 if (!pgUser.password_hash) {
472 console.log('[JWT Auth] User has no password hash (OAuth-only user?)');
473 return callback(null);
476 // Verify password using bcrypt
477 const bcrypt = require('bcrypt');
478 const passwordMatch = await bcrypt.compare(password, pgUser.password_hash);
480 if (!passwordMatch) {
481 console.log('[JWT Auth] Password verification failed for:', username);
482 return callback(null);
485 console.log('[JWT Auth] Password verified successfully for:', username);
487 // Determine site admin privileges
489 if (pgUser.role === 'admin') {
490 // Check if root tenant
491 if (pgUser.tenant_id === '1' || pgUser.tenant_id === '00000000-0000-0000-0000-000000000001') {
492 siteadmin = 0xFFFFFFFF; // Full admin
494 siteadmin = 0x00000006; // Manage users + server update
498 // Map PostgreSQL user to MeshCentral user format
500 _id: `user//${pgUser.email}`,
502 name: pgUser.name || pgUser.email.split('@')[0],
504 siteadmin: siteadmin,
506 creation: Math.floor(new Date(pgUser.created_at).getTime() / 1000),
509 _postgres_user_id: pgUser.user_id,
510 _tenant_id: pgUser.tenant_id
513 console.log(`[JWT Auth] User authenticated: ${meshUser._id}, siteadmin=${siteadmin}`);
515 // Fetch device links for this user's tenant
516 obj.getUserDeviceLinks(pgUser.user_id, pgUser.tenant_id, (links) => {
517 meshUser.links = links;
522 console.error('❌ JWT Auth: Password authentication error:', err.message);
530 obj.healthCheck = async function (callback) {
532 const result = await obj.pool.query('SELECT NOW() as time, COUNT(*) as user_count FROM users');
535 database: 'connected',
536 users: result.rows[0].user_count,
537 cache_size: obj.userCache.size,
538 timestamp: result.rows[0].time
543 database: 'disconnected',
549 // Initialize on creation